Build a User Management System in Next.js with TailAdmin (Full Step-By-Step Guide)Build a User Management System in Next.js with TailAdmin (Full Step-By-Step Guide)

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).

Live Demo

For react js new comers, please check the below links:

  1. Reactjs Tutorials
  2. Nextjs
  3. Bootstrap 5
  4. React Free Ecommerce Templates
  5. React Free Admins

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:

GitHub link

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

By therichpost

Hello to all. Welcome to therichpost.com. Myself Ajay Malhotra and I am freelance full stack developer. I love coding. I know WordPress, Core php, Angularjs, Angular 19, MedusaJs, Next.js, Bootstrap 5, Nodejs, Laravel, Codeigniter, Shopify, Squarespace, jQuery, Google Map Api, Vuejs, Reactjs, Big commerce etc.