Hello guys, how are you? Welcome back to my blog therichpost.com. Today in this post I will tell you Build a User Management System in Next.js with TailAdmin (Full Step-By-Step Guide).
For react js new comers, please check the below links:
What We’ll Build
By the end of this tutorial, you’ll have a complete User Management Module with:
✔ User Table
✔ Add User Modal (Popup)
✔ Edit User Modal
✔ Delete User
✔ Status badges
✔ Role selection
✔ Real-time search
✔ Data saved in LocalStorage
✔ Beautiful TailAdmin UI
✔ Fully typed with TypeScript
Guys first here is GitHub link from where I downloaded this free admin dashboard template:
Guys now here is custom code:
Step 1 — Create the User Management Page:
Create a new folder users inside src/app/(admin) folder and then create page.tsx file inside src/app/(admin)/users folder and add below code inside it:
"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
type Role = "Admin" | "User" | "Manager";
type Status = "Active" | "Pending" | "Inactive";
interface User {
id: number;
name: string;
email: string;
role: Role;
status: Status;
}
const STORAGE_KEY = "tailadmin_users_v1";
const ALT_KEYS = ["tailadmin_users", "tailadmin_users_v0"];
const INITIAL_USERS: User[] = [
{ id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Admin", status: "Active" },
{ id: 2, name: "Bob Smith", email: "bob@example.com", role: "User", status: "Inactive" },
{ id: 3, name: "Cecilia Brown", email: "cecilia@example.com", role: "Manager", status: "Active" },
];
function uid(): number {
return Date.now() + Math.floor(Math.random() * 1000);
}
export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
const [query, setQuery] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<User | null>(null);
useEffect(() => {
try {
let raw: string | null = null;
let foundKey: string | null = null;
const keysToCheck = [STORAGE_KEY, ...ALT_KEYS];
for (const k of keysToCheck) {
const v = localStorage.getItem(k);
if (v) {
raw = v;
foundKey = k;
break;
}
}
if (!raw) {
console.info("[UsersPage] no localStorage entry found — seeding INITIAL_USERS");
localStorage.setItem(STORAGE_KEY, JSON.stringify(INITIAL_USERS));
setUsers(INITIAL_USERS);
return;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (parseErr) {
console.warn("[UsersPage] stored JSON is invalid — resetting to INITIAL_USERS", parseErr);
localStorage.setItem(STORAGE_KEY, JSON.stringify(INITIAL_USERS));
setUsers(INITIAL_USERS);
return;
}
if (!Array.isArray(parsed)) {
console.warn("[UsersPage] parsed value is not an array — resetting to INITIAL_USERS", parsed);
localStorage.setItem(STORAGE_KEY, JSON.stringify(INITIAL_USERS));
setUsers(INITIAL_USERS);
return;
}
if ((parsed as any[]).length === 0) {
console.info("[UsersPage] parsed array empty — seeding INITIAL_USERS");
localStorage.setItem(STORAGE_KEY, JSON.stringify(INITIAL_USERS));
setUsers(INITIAL_USERS);
return;
}
if (foundKey && foundKey !== STORAGE_KEY) {
console.info(`[UsersPage] migrating users from ${foundKey} -> ${STORAGE_KEY}`);
try {
localStorage.setItem(STORAGE_KEY, raw);
} catch (e) {
console.warn("[UsersPage] migration write failed", e);
}
}
setUsers(parsed as User[]);
console.info(`[UsersPage] loaded ${ (parsed as any[]).length } users from storage (${foundKey ?? STORAGE_KEY})`);
} catch (err) {
console.error("[UsersPage] unexpected error while loading users, seeding fallback", err);
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(INITIAL_USERS)); } catch {}
setUsers(INITIAL_USERS);
}
}, []);
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(users));
} catch (err) {
console.error("[UsersPage] failed to persist users to localStorage", err);
}
}, [users]);
const openAdd = () => {
setEditing(null);
setModalOpen(true);
};
const openEdit = (u: User) => {
setEditing(u);
setModalOpen(true);
};
const handleDelete = (u: User) => {
if (!confirm(`Delete ${u.name}?`)) return;
setUsers((prev) => prev.filter((x) => x.id !== u.id));
};
const handleSave = (payload: Omit<User, "id">) => {
if (editing) {
setUsers((prev) => prev.map((p) => (p.id === editing.id ? { ...p, ...payload } : p)));
} else {
const newUser: User = { id: uid(), ...payload };
setUsers((prev) => [newUser, ...prev]);
}
setModalOpen(false);
setEditing(null);
};
const filtered = users.filter((u) => {
const q = query.trim().toLowerCase();
if (!q) return true;
return (
u.name.toLowerCase().includes(q) ||
u.email.toLowerCase().includes(q) ||
u.role.toLowerCase().includes(q) ||
u.status.toLowerCase().includes(q)
);
});
return (
<main className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-indigo-600 text-white flex items-center justify-center font-semibold">U</div>
<h1 className="text-2xl font-semibold">User Management</h1>
</div>
<div className="flex items-center gap-3">
<input
type="text"
placeholder="Search by name, email or role"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="px-3 py-2 border rounded-md text-sm w-72"
aria-label="Search users"
/>
<button onClick={openAdd} className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z"/></svg>
Add User
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500">Role</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500">Status</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y">
{filtered.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-6 text-center text-sm text-gray-500">No users found.</td>
</tr>
) : (
filtered.map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-indigo-500 text-white flex items-center justify-center font-medium">{u.name.charAt(0)}</div>
<div>
<div className="text-sm font-medium text-gray-900">{u.name}</div>
<div className="text-xs text-gray-500">{u.email}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{u.email}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{u.role}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${u.status === "Active" ? "bg-green-100 text-green-800" : ""} ${u.status === "Pending" ? "bg-yellow-100 text-yellow-800" : ""} ${u.status === "Inactive" ? "bg-gray-100 text-gray-700" : ""}`}>{u.status}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onClick={() => openEdit(u)} className="inline-flex items-center p-1 rounded hover:bg-gray-100 mr-2" title="Edit" aria-label={`Edit ${u.name}`}>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0L15.13 4.9l3.75 3.75 1.83-1.61z"/></svg>
</button>
<button onClick={() => handleDelete(u)} className="inline-flex items-center p-1 rounded hover:bg-gray-100 text-red-600" title="Delete" aria-label={`Delete ${u.name}`}>
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M6 19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Modal */}
{modalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/40" onClick={() => { setModalOpen(false); setEditing(null); }} aria-hidden="true"></div>
<div className="bg-white rounded-lg shadow-lg z-50 w-full max-w-lg mx-4">
<div className="p-4 border-b flex items-center justify-between">
<h3 className="text-lg font-medium">{editing ? "Edit User" : "Add User"}</h3>
<button onClick={() => { setModalOpen(false); setEditing(null); }} className="text-gray-500 hover:text-gray-700" aria-label="Close modal">✕</button>
</div>
<UserForm initial={editing} onCancel={() => { setModalOpen(false); setEditing(null); }} onSave={handleSave} />
</div>
</div>
)}
</main>
);
}
type UserFormProps = {
initial: User | null;
onSave: (payload: Omit<User, "id">) => void;
onCancel: () => void;
};
function UserForm({ initial, onSave, onCancel }: UserFormProps) {
const [name, setName] = useState(initial?.name ?? "");
const [email, setEmail] = useState(initial?.email ?? "");
const [role, setRole] = useState<Role>(initial?.role ?? "User");
const [status, setStatus] = useState<Status>(initial?.status ?? "Active");
useEffect(() => {
setName(initial?.name ?? "");
setEmail(initial?.email ?? "");
setRole(initial?.role ?? "User");
setStatus(initial?.status ?? "Active");
}, [initial]);
const submit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim() || !email.trim()) {
alert("Name and email are required.");
return;
}
onSave({ name: name.trim(), email: email.trim(), role, status });
};
return (
<form onSubmit={submit}>
<div className="p-4">
<div className="mb-3">
<label className="block text-sm font-medium mb-1">Full name</label>
<input value={name} onChange={(e) => setName(e.target.value)} className="w-full border rounded px-3 py-2" aria-label="Full name" />
</div>
<div className="mb-3">
<label className="block text-sm font-medium mb-1">Email</label>
<input value={email} onChange={(e) => setEmail(e.target.value)} className="w-full border rounded px-3 py-2" aria-label="Email" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium mb-1">Role</label>
<select value={role} onChange={(e) => setRole(e.target.value as Role)} className="w-full border rounded px-2 py-2" aria-label="Role">
<option>Admin</option>
<option>User</option>
<option>Manager</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select value={status} onChange={(e) => setStatus(e.target.value as Status)} className="w-full border rounded px-2 py-2" aria-label="Status">
<option>Active</option>
<option>Pending</option>
<option>Inactive</option>
</select>
</div>
</div>
<div className="mt-4 flex justify-end gap-2">
<button type="button" onClick={onCancel} className="px-3 py-2 rounded border">Cancel</button>
<button type="submit" className="px-3 py-2 rounded bg-indigo-600 text-white">Save</button>
</div>
</div>
</form>
);
}
Step 2: Guys to add the link inside sidebar we need to add below code inside app/layout/AppSidebar.tsx file:
...
{
icon: <UserCircleIcon />,
name: "User Profile",
path: "/profile",
},
{
icon: <UserIcon />,
name: "User Managment",
path: "/users",
},
...
Final Result: A Fully Functional User Management System
Conclusion
You now have a complete User Management Page inside your Next.js TailAdmin Dashboard!
This feature alone instantly makes your admin panel feel like a real SaaS product.
Ajay
Thanks
