Every application needs an admin interface. Here is how to build a production-ready one with Next.js.
Step 1: Admin Layout with Sidebar
// app/admin/layout.tsx
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { AdminSidebar } from "@/components/admin/AdminSidebar";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session?.user) redirect("/login");
if (session.user.role !== "admin") redirect("/");
return (
<div className="flex min-h-screen">
<AdminSidebar user={session.user} />
<main className="flex-1 overflow-auto bg-gray-50 p-6 dark:bg-gray-950">
{children}
</main>
</div>
);
}
Step 2: Sidebar Navigation
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
LayoutDashboard,
Users,
Package,
FileText,
Settings,
BarChart,
LogOut,
} from "lucide-react";
const navItems = [
{ href: "/admin", label: "Dashboard", icon: LayoutDashboard },
{ href: "/admin/users", label: "Users", icon: Users },
{ href: "/admin/products", label: "Products", icon: Package },
{ href: "/admin/orders", label: "Orders", icon: FileText },
{ href: "/admin/analytics", label: "Analytics", icon: BarChart },
{ href: "/admin/settings", label: "Settings", icon: Settings },
];
export function AdminSidebar({ user }: { user: { name: string; email: string } }) {
const pathname = usePathname();
return (
<aside className="flex w-64 flex-col border-r bg-white dark:border-gray-800 dark:bg-gray-900">
{/* Logo */}
<div className="border-b p-4 dark:border-gray-800">
<h1 className="text-lg font-bold">Admin Panel</h1>
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1 p-3">
{navItems.map(({ href, label, icon: Icon }) => {
const isActive = pathname === href ||
(href !== "/admin" && pathname.startsWith(href));
return (
<Link
key={href}
href={href}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
isActive
? "bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
}`}
>
<Icon className="h-4 w-4" />
{label}
</Link>
);
})}
</nav>
{/* User info */}
<div className="border-t p-3 dark:border-gray-800">
<div className="flex items-center gap-3 rounded-lg px-3 py-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-sm font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
{user.name?.charAt(0) || "A"}
</div>
<div className="flex-1">
<p className="text-sm font-medium">{user.name}</p>
<p className="text-xs text-gray-500">{user.email}</p>
</div>
</div>
</div>
</aside>
);
}
Step 3: Data Table Component
"use client";
import { useState } from "react";
import { ChevronUp, ChevronDown, Search, Plus } from "lucide-react";
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
onAdd?: () => void;
onEdit?: (row: T) => void;
onDelete?: (row: T) => void;
searchable?: boolean;
}
export function DataTable<T extends { id: string }>({
data,
columns,
onAdd,
onEdit,
onDelete,
searchable = true,
}: DataTableProps<T>) {
const [search, setSearch] = useState("");
const [sortKey, setSortKey] = useState<keyof T | null>(null);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const [page, setPage] = useState(0);
const pageSize = 10;
// Filter
const filtered = data.filter((row) =>
search
? Object.values(row).some((v) =>
String(v).toLowerCase().includes(search.toLowerCase())
)
: true
);
// Sort
const sorted = sortKey
? [...filtered].sort((a, b) => {
const aVal = a[sortKey];
const bVal = b[sortKey];
const cmp = String(aVal).localeCompare(String(bVal));
return sortDir === "asc" ? cmp : -cmp;
})
: filtered;
// Paginate
const paged = sorted.slice(page * pageSize, (page + 1) * pageSize);
const totalPages = Math.ceil(sorted.length / pageSize);
function toggleSort(key: keyof T) {
if (sortKey === key) {
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
} else {
setSortKey(key);
setSortDir("asc");
}
}
return (
<div>
{/* Toolbar */}
<div className="mb-4 flex items-center justify-between">
{searchable && (
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<input
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(0);
}}
placeholder="Search..."
className="rounded-lg border pl-9 pr-4 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
/>
</div>
)}
{onAdd && (
<button
onClick={onAdd}
className="flex items-center gap-1 rounded-lg bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700"
>
<Plus className="h-4 w-4" /> Add New
</button>
)}
</div>
{/* Table */}
<div className="overflow-x-auto rounded-lg border dark:border-gray-700">
<table className="w-full text-sm">
<thead className="border-b bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
<tr>
{columns.map((col) => (
<th
key={String(col.key)}
onClick={() => col.sortable && toggleSort(col.key)}
className={`px-4 py-3 text-left font-medium ${
col.sortable ? "cursor-pointer select-none" : ""
}`}
>
<div className="flex items-center gap-1">
{col.label}
{col.sortable && sortKey === col.key && (
sortDir === "asc" ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)
)}
</div>
</th>
))}
{(onEdit || onDelete) && (
<th className="px-4 py-3 text-right">Actions</th>
)}
</tr>
</thead>
<tbody>
{paged.map((row) => (
<tr
key={row.id}
className="border-b last:border-0 hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/50"
>
{columns.map((col) => (
<td key={String(col.key)} className="px-4 py-3">
{col.render
? col.render(row[col.key], row)
: String(row[col.key])}
</td>
))}
{(onEdit || onDelete) && (
<td className="px-4 py-3 text-right">
<div className="flex justify-end gap-2">
{onEdit && (
<button
onClick={() => onEdit(row)}
className="text-blue-600 hover:underline"
>
Edit
</button>
)}
{onDelete && (
<button
onClick={() => onDelete(row)}
className="text-red-600 hover:underline"
>
Delete
</button>
)}
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="mt-4 flex items-center justify-between text-sm text-gray-500">
<span>
Showing {page * pageSize + 1}-{Math.min((page + 1) * pageSize, sorted.length)} of {sorted.length}
</span>
<div className="flex gap-1">
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="rounded border px-3 py-1 disabled:opacity-50 dark:border-gray-700"
>
Previous
</button>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="rounded border px-3 py-1 disabled:opacity-50 dark:border-gray-700"
>
Next
</button>
</div>
</div>
</div>
);
}
Step 4: Dashboard Overview Page
// app/admin/page.tsx
import { db } from "@/db";
import { users, orders, products } from "@/db/schema";
import { count, sum } from "drizzle-orm";
import { Users, Package, DollarSign, TrendingUp } from "lucide-react";
export default async function AdminDashboard() {
const [userCount] = await db.select({ count: count() }).from(users);
const [orderStats] = await db
.select({ count: count(), revenue: sum(orders.amount) })
.from(orders);
const [productCount] = await db.select({ count: count() }).from(products);
const stats = [
{ label: "Total Users", value: userCount.count, icon: Users, color: "blue" },
{ label: "Total Orders", value: orderStats.count, icon: Package, color: "green" },
{ label: "Revenue", value: `$${orderStats.revenue}`, icon: DollarSign, color: "purple" },
{ label: "Products", value: productCount.count, icon: TrendingUp, color: "orange" },
];
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat) => (
<div
key={stat.label}
className="rounded-xl border bg-white p-6 dark:border-gray-800 dark:bg-gray-900"
>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">{stat.label}</p>
<stat.icon className="h-5 w-5 text-gray-400" />
</div>
<p className="mt-2 text-3xl font-bold">{stat.value}</p>
</div>
))}
</div>
</div>
);
}
Need a Custom Admin Panel?
We build admin dashboards and internal tools with data management, analytics, and role-based access. Contact us to discuss your project.