learnix / src /app /admin /users /AdminUsers.js
shashidharak99's picture
Upload files
7d51e81 verified
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import Image from "next/image";
import {
FiUsers, FiArrowLeft, FiShield, FiUser, FiLoader,
FiChevronDown, FiCalendar, FiHash, FiUserCheck, FiUserX,
} from "react-icons/fi";
import { MdAdminPanelSettings } from "react-icons/md";
import "../styles/AdminDashboard.css";
import "./AdminUsers.css";
const PAGE_LIMIT = 12;
function RoleBadge({ role }) {
if (role === "superadmin")
return <span className="au-role-badge au-role-superadmin"><FiShield size={11} /> Super Admin</span>;
if (role === "admin")
return <span className="au-role-badge au-role-admin"><MdAdminPanelSettings size={11} /> Admin</span>;
return <span className="au-role-badge au-role-user"><FiUser size={11} /> User</span>;
}
function formatDate(dateStr) {
if (!dateStr) return "—";
return new Date(dateStr).toLocaleDateString("en-IN", {
day: "2-digit", month: "short", year: "numeric",
});
}
export default function AdminUsers() {
const [myRole, setMyRole] = useState(null);
const [token, setToken] = useState("");
const [myUsn, setMyUsn] = useState("");
const [isLoaded, setIsLoaded] = useState(false);
const [users, setUsers] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState("");
const [changingRole, setChangingRole] = useState({});
const [roleMsg, setRoleMsg] = useState({});
const [openConfirmUsn, setOpenConfirmUsn] = useState(null);
const [confirmAction, setConfirmAction] = useState(null);
const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0 });
const confirmRef = useRef(null);
const btnRefs = useRef({});
// ── bootstrap ──
useEffect(() => {
const r = localStorage.getItem("role") || "";
const t = localStorage.getItem("token") || "";
const u = localStorage.getItem("usn") || "";
setMyRole(r); setToken(t); setMyUsn(u);
setTimeout(() => setIsLoaded(true), 100);
}, []);
// ── fetch users ──
const fetchUsers = useCallback(async (pageNum, append = false) => {
if (!token) return;
append ? setLoadingMore(true) : setLoading(true);
setError("");
try {
const res = await fetch(`/api/admin/users?page=${pageNum}&limit=${PAGE_LIMIT}`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
if (!res.ok) { setError(data.error || "Failed to fetch users"); return; }
append ? setUsers(prev => [...prev, ...data.users]) : setUsers(data.users);
setTotal(data.total);
setPage(data.page);
setTotalPages(data.totalPages);
} catch {
setError("Network error. Please try again.");
} finally {
setLoading(false); setLoadingMore(false);
}
}, [token]);
useEffect(() => {
if (token && (myRole === "admin" || myRole === "superadmin")) fetchUsers(1, false);
}, [token, myRole, fetchUsers]);
const handleViewMore = () => fetchUsers(page + 1, true);
// ── inline confirm helpers ──
const openConfirm = (user, newRole, usn) => {
const btn = btnRefs.current[usn];
if (btn) {
const rect = btn.getBoundingClientRect();
const dropH = 190;
const dropW = 230;
const spaceBelow = window.innerHeight - rect.bottom;
const top = spaceBelow > dropH ? rect.bottom + 8 : rect.top - dropH - 8;
const spaceRight = window.innerWidth - rect.left;
const left = spaceRight > dropW ? rect.left : rect.right - dropW;
setDropdownPos({ top, left });
}
setOpenConfirmUsn(user.usn);
setConfirmAction({ user, newRole });
};
const closeConfirm = () => { setOpenConfirmUsn(null); setConfirmAction(null); };
const handleConfirmed = () => {
if (!confirmAction) return;
handleRoleChange(confirmAction.user, confirmAction.newRole);
closeConfirm();
};
// close on outside click
useEffect(() => {
if (!openConfirmUsn) return;
const handler = (e) => {
if (confirmRef.current && !confirmRef.current.contains(e.target)) {
const btns = Object.values(btnRefs.current);
if (btns.some(b => b && b.contains(e.target))) return;
closeConfirm();
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [openConfirmUsn]);
// ── change role ──
const handleRoleChange = async (user, newRole) => {
setChangingRole(prev => ({ ...prev, [user.usn]: true }));
setRoleMsg(prev => ({ ...prev, [user.usn]: "" }));
try {
const res = await fetch("/api/admin/users/role", {
method: "PATCH",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ targetUsn: user.usn, newRole }),
});
const data = await res.json();
if (!res.ok) { setRoleMsg(prev => ({ ...prev, [user.usn]: data.error || "Failed" })); return; }
setUsers(prev => prev.map(u => u.usn === user.usn ? { ...u, role: data.user.role } : u));
setRoleMsg(prev => ({
...prev,
[user.usn]: newRole === "admin" ? "Made admin ✓" : "Removed admin ✓",
}));
setTimeout(() => setRoleMsg(prev => ({ ...prev, [user.usn]: "" })), 2500);
} catch {
setRoleMsg(prev => ({ ...prev, [user.usn]: "Network error" }));
} finally {
setChangingRole(prev => ({ ...prev, [user.usn]: false }));
}
};
// ── guards ──
if (myRole === null) return null;
const isSuperAdmin = myRole === "superadmin";
return (
<div className={`adm-wrapper ${isLoaded ? "adm-loaded" : ""}`}>
{/* ── Header ── */}
<header className="adm-header">
<div className="adm-header-content">
<div className="adm-header-left">
<Link href="/admin" className="au-back-link">
<FiArrowLeft size={15} /> Admin Dashboard
</Link>
<div className="adm-role-badge" style={{ backgroundColor: "#3b82f6", marginTop: "0.75rem" }}>
<FiUsers size={16} />
<span>User Management</span>
</div>
<h1 className="adm-title">
All <span className="adm-title-highlight">Users</span>
</h1>
<p className="adm-subtitle">
Latest registered users — {total > 0 ? `${total} total` : "loading…"}
</p>
</div>
<div className="adm-header-deco">
<div className="adm-deco-circle" style={{ background: "#dbeafe", border: "2px solid #bfdbfe" }}>
<FiUsers size={44} color="#3b82f6" />
</div>
</div>
</div>
</header>
{/* ── Summary strip ── */}
<section className="adm-summary">
<div className="adm-summary-card adm-summary-blue">
<div className="adm-summary-icon"><FiUsers size={20} /></div>
<div>
<div className="adm-summary-value">{total}</div>
<div className="adm-summary-label">Total Users</div>
</div>
</div>
<div className="adm-summary-card adm-summary-green">
<div className="adm-summary-icon"><FiUserCheck size={20} /></div>
<div>
<div className="adm-summary-value">{users.filter(u => (u.role || "user") === "admin").length}</div>
<div className="adm-summary-label">Admins (loaded)</div>
</div>
</div>
<div className="adm-summary-card adm-summary-gray">
<div className="adm-summary-icon"><FiUser size={20} /></div>
<div>
<div className="adm-summary-value">{users.filter(u => !u.role || u.role === "user").length}</div>
<div className="adm-summary-label">Regular Users</div>
</div>
</div>
<div className="adm-summary-card adm-summary-yellow">
<div className="adm-summary-icon"><FiHash size={20} /></div>
<div>
<div className="adm-summary-value">{users.length}</div>
<div className="adm-summary-label">Loaded</div>
</div>
</div>
</section>
{/* ── Error ── */}
{error && <div className="au-error-banner">{error}</div>}
{/* ── User list ── */}
{loading ? (
<div className="au-loading-state">
<div className="au-spinner"><FiLoader size={28} /></div>
<p>Loading users…</p>
</div>
) : (
<>
<section className="au-users-grid">
{users.map((user, idx) => {
const userRole = user.role || "user";
const isChanging = changingRole[user.usn];
const msg = roleMsg[user.usn];
const isOwnAccount = user.usn === myUsn;
const isOpen = openConfirmUsn === user.usn;
return (
<div
key={user._id || idx}
className="au-user-card"
style={{ animationDelay: `${idx * 0.04}s` }}
>
{/* Avatar + name row */}
<div className="au-card-top">
<div className="au-avatar-wrap">
<Image
src={user.profileimg || "https://res.cloudinary.com/dihocserl/image/upload/v1758109403/profile-blue-icon_w3vbnt.webp"}
alt={user.name}
width={48} height={48}
className="au-avatar"
/>
</div>
<div className="au-card-info">
<p className="au-user-name">{user.name}</p>
<p className="au-user-usn">{user.usn}</p>
</div>
<RoleBadge role={userRole} />
</div>
{/* Meta row */}
<div className="au-meta-row">
<span className="au-meta-item">
<FiCalendar size={12} /> Joined {formatDate(user.createdAt)}
</span>
</div>
{/* Role action — superadmin only, not own account */}
{isSuperAdmin && !isOwnAccount && (
<div className="au-role-action">
<div className="au-confirm-wrap">
{userRole === "admin" ? (
<button
ref={el => { btnRefs.current[user.usn] = el; }}
className="au-role-btn au-role-btn-remove"
onClick={() => isOpen ? closeConfirm() : openConfirm(user, "user", user.usn)}
disabled={isChanging}
>
<FiUserX size={14} />
{isChanging ? "Updating…" : "Remove Admin"}
</button>
) : (
<button
ref={el => { btnRefs.current[user.usn] = el; }}
className="au-role-btn au-role-btn-make"
onClick={() => isOpen ? closeConfirm() : openConfirm(user, "admin", user.usn)}
disabled={isChanging}
>
<FiUserCheck size={14} />
{isChanging ? "Updating…" : "Make Admin"}
</button>
)}
</div>
{msg && <span className="au-role-msg">{msg}</span>}
</div>
)}
{/* Own account label */}
{isOwnAccount && <div className="au-own-tag">You</div>}
</div>
);
})}
</section>
{/* ── View more ── */}
{page < totalPages && (
<div className="au-view-more-wrap">
<button className="au-view-more-btn" onClick={handleViewMore} disabled={loadingMore}>
{loadingMore ? (
<><span className="au-dots"><span /><span /><span /></span> Loading…</>
) : (
<><FiChevronDown size={16} /> View More ({total - users.length} remaining)</>
)}
</button>
</div>
)}
{users.length > 0 && page >= totalPages && (
<p className="au-all-loaded">All {total} users loaded</p>
)}
{users.length === 0 && !loading && (
<div className="au-empty"><FiUsers size={36} /><p>No users found</p></div>
)}
</>
)}
{/* ── Portal confirm dropdown ── renders on document.body, floats above everything ── */}
{openConfirmUsn && confirmAction && typeof document !== "undefined" &&
createPortal(
<div
ref={confirmRef}
className="au-confirm-dropdown"
style={{ top: dropdownPos.top, left: dropdownPos.left }}
>
<div className={`au-confirm-dropdown-icon ${
confirmAction.newRole === "admin" ? "au-confirm-icon-blue" : "au-confirm-icon-red"
}`}>
{confirmAction.newRole === "admin" ? <FiUserCheck size={18} /> : <FiUserX size={18} />}
</div>
<p className="au-confirm-dropdown-title">
{confirmAction.newRole === "admin" ? "Make Admin?" : "Remove Admin?"}
</p>
<p className="au-confirm-dropdown-text">
{confirmAction.newRole === "admin"
? <><strong>{confirmAction.user.name}</strong> will gain admin access.</>
: <><strong>{confirmAction.user.name}</strong> will lose admin access.</>}
</p>
<div className="au-confirm-dropdown-actions">
<button className="au-confirm-dropdown-btn au-confirm-cancel" onClick={closeConfirm}>
Cancel
</button>
<button
className={`au-confirm-dropdown-btn ${
confirmAction.newRole === "admin" ? "au-confirm-ok-blue" : "au-confirm-ok-red"
}`}
onClick={handleConfirmed}
>
Confirm
</button>
</div>
</div>,
document.body
)
}
</div>
);
}