375 lines
17 KiB
TypeScript
Raw Normal View History

2025-05-12 12:20:28 +01:00
"use client";
import React, { useState, useRef } from "react";
2025-05-19 12:28:38 +01:00
type Role = "ADMIN" | "GUEST" | "SCIENTIST";
const roleLabels: Record<Role, string> = {
ADMIN: "Admin",
GUEST: "Guest",
SCIENTIST: "Scientist",
2025-05-19 12:28:38 +01:00
};
type User = {
2025-05-19 12:28:38 +01:00
id: number;
email: string;
name: string;
role: Role;
password: string;
createdAt: string;
2025-05-19 12:28:38 +01:00
};
const initialUsers: User[] = [
{ email: "john@example.com", name: "John Doe", role: "ADMIN", password: "secret1", createdAt: "2024-06-21T09:15:01Z" ,id:1},
{ email: "jane@example.com", name: "Jane Smith", role: "GUEST", password: "secret2", createdAt: "2024-06-21T10:01:09Z" ,id:2},
{ email: "bob@example.com", name: "Bob Brown", role: "SCIENTIST", password: "secret3", createdAt: "2024-06-21T12:13:45Z" ,id:3},
{ email: "alice@example.com", name: "Alice Johnson",role: "GUEST", password: "secret4", createdAt: "2024-06-20T18:43:20Z" ,id:4},
{ email: "eve@example.com", name: "Eve Black", role: "ADMIN", password: "secret5", createdAt: "2024-06-20T19:37:10Z" ,id:5},
{ email: "dave@example.com", name: "Dave Clark", role: "GUEST", password: "pw", createdAt: "2024-06-19T08:39:10Z" ,id:6},
{ email: "fred@example.com", name: "Fred Fox", role: "GUEST", password: "pw", createdAt: "2024-06-19T09:11:52Z" ,id:7},
{ email: "ginny@example.com", name: "Ginny Hall", role: "SCIENTIST", password: "pw", createdAt: "2024-06-17T14:56:27Z" ,id:8},
{ email: "harry@example.com", name: "Harry Lee", role: "ADMIN", password: "pw", createdAt: "2024-06-16T19:28:11Z" ,id:9},
{ email: "ivy@example.com", name: "Ivy Volt", role: "ADMIN", password: "pw", createdAt: "2024-06-15T21:04:05Z" ,id:10},
{ email: "kate@example.com", name: "Kate Moss", role: "SCIENTIST", password: "pw", createdAt: "2024-06-14T11:16:35Z" ,id:11},
{ email: "leo@example.com", name: "Leo Garrison", role: "GUEST", password: "pw", createdAt: "2024-06-12T08:02:51Z" ,id:12},
{ email: "isaac@example.com", name: "Isaac Yang", role: "GUEST", password: "pw", createdAt: "2024-06-12T15:43:29Z" ,id:13},
2025-05-19 12:28:38 +01:00
];
const sortFields = [ // Sort box options
2025-05-12 12:20:28 +01:00
{ label: "Name", value: "name" },
{ label: "Email", value: "email" },
2025-05-12 12:20:28 +01:00
] as const;
type SortField = typeof sortFields[number]["value"];
2025-05-12 12:20:28 +01:00
type SortDir = "asc" | "desc";
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
const fieldLabels: Record<SortField, string> = { name: "Name", email: "Email" };
export default function AdminPage() {
const [users, setUsers] = useState<User[]>(initialUsers);
const [selectedEmail, setSelectedEmail] = useState<string | null>(null);
2025-05-12 12:20:28 +01:00
// Local edit state for SCIENTIST form
const [editUser, setEditUser] = useState<User | null>(null);
// Reset editUser when the selected user changes
2025-05-12 12:20:28 +01:00
React.useEffect(() => {
if (!selectedEmail) setEditUser(null);
else {
const user = users.find(u => u.email === selectedEmail);
setEditUser(user ? { ...user } : null);
2025-05-12 12:20:28 +01:00
}
}, [selectedEmail, users]);
// Search/filter/sort state
const [searchField, setSearchField] = useState<"name" | "email">("name");
2025-05-12 12:20:28 +01:00
const [searchText, setSearchText] = useState("");
const [roleFilter, setRoleFilter] = useState<Role | "all">("all");
const [sortField, setSortField] = useState<SortField>("name");
const [sortDir, setSortDir] = useState<SortDir>("asc");
// Dropdown states
2025-05-12 12:20:28 +01:00
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
const [sortDropdownOpen, setSortDropdownOpen] = useState(false);
const filterDropdownRef = useRef<HTMLDivElement>(null);
const sortDropdownRef = useRef<HTMLDivElement>(null);
React.useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (
filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)
) setFilterDropdownOpen(false);
if (
sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)
) setSortDropdownOpen(false);
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Filtering, searching, sorting logic
2025-05-12 12:20:28 +01:00
const filteredUsers = users.filter(
(user) => roleFilter === "all" || user.role === roleFilter
);
const searchedUsers = filteredUsers.filter(user =>
user[searchField].toLowerCase().includes(searchText.toLowerCase())
);
const sortedUsers = [...searchedUsers].sort((a, b) => {
let cmp = a[sortField].localeCompare(b[sortField]);
return sortDir === "asc" ? cmp : -cmp;
});
// Form input change handler
2025-05-12 12:20:28 +01:00
const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
if (!editUser) return;
const { name, value } = e.target;
setEditUser(prev =>
prev ? { ...prev, [name]: value } : null
);
};
// Update button logic (compare original selectedUser and editUser)
const selectedUser = users.find((u) => u.email === selectedEmail);
2025-05-12 12:20:28 +01:00
const isEditChanged = React.useMemo(() => {
if (!editUser || !selectedUser) return false;
// Compare primitive fields
2025-05-12 12:20:28 +01:00
return (
editUser.name !== selectedUser.name ||
editUser.role !== selectedUser.role ||
editUser.password !== selectedUser.password
2025-05-12 12:20:28 +01:00
);
}, [editUser, selectedUser]);
// Update/save changes
const handleUpdate = (e: React.FormEvent) => {
e.preventDefault();
if (!editUser) return;
setUsers(prev =>
prev.map(u =>
u.email === editUser.email ? { ...editUser } : u
2025-05-12 12:20:28 +01:00
)
);
// After successful update, update selectedUser local state
// (editUser will auto-sync due to useEffect on users)
2025-05-12 12:20:28 +01:00
};
// Delete user logic
2025-05-12 12:20:28 +01:00
const handleDelete = () => {
if (!selectedUser) return;
if (!window.confirm(`Are you sure you want to delete "${selectedUser.name}"? This cannot be undone.`)) return;
setUsers(prev => prev.filter(u => u.email !== selectedUser.email));
setSelectedEmail(null);
setEditUser(null);
};
const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"];
// Tooltip handling for email field
2025-05-12 12:20:28 +01:00
const [showEmailTooltip, setShowEmailTooltip] = useState(false);
return (
<div className="flex flex-col h-full">
<div className="flex h-full overflow-hidden bg-gray-50">
{/* SIDEBAR */}
<div className="w-80 h-full border-r border-neutral-200 bg-neutral-100 flex flex-col rounded-l-xl shadow-sm">
2025-05-12 12:20:28 +01:00
<div className="p-4 flex flex-col h-full">
{/* Search Bar */}
<div className="mb-3 flex gap-2">
<input
2025-05-12 12:20:28 +01:00
className="flex-1 border rounded-lg px-2 py-1 text-sm"
placeholder={`Search by ${searchField}`}
value={searchText}
onChange={e => setSearchText(e.target.value)}
/>
<button
type="button"
className="border rounded-lg px-2 py-1 text-sm bg-white hover:bg-neutral-100 transition font-semibold"
style={{ width: "80px" }} // fixed width, adjust as needed
onClick={() => setSearchField(field => field === "name" ? "email" : "name")}
title={`Switch to searching by ${searchField === "name" ? "Email" : "Name"}`}
>
{searchField === "name" ? "Email" : "Name"}
</button>
2025-05-12 12:20:28 +01:00
</div>
{/* Filter and Sort Buttons */}
<div className="flex gap-2 items-center mb-2">
{/* Filter */}
<div className="relative" ref={filterDropdownRef}>
<button
className={`px-3 py-1 rounded-lg border font-semibold flex items-center transition
${roleFilter !== "all"
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700"
: "bg-white text-gray-700 border hover:bg-neutral-200"}
2025-05-12 12:20:28 +01:00
`}
onClick={() => setFilterDropdownOpen(v => !v)}
type="button"
>
Filter <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg>
2025-05-12 12:20:28 +01:00
</button>
{filterDropdownOpen && (
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
<button
onClick={() => { setRoleFilter("all"); setFilterDropdownOpen(false); }}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${roleFilter==="all" ? "font-bold text-blue-600" : ""}`}
2025-05-12 12:20:28 +01:00
>
All
</button>
{allRoles.map(role => (
<button
key={role}
onClick={() => { setRoleFilter(role); setFilterDropdownOpen(false); }}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${roleFilter===role ? "font-bold text-blue-600" : ""}`}
2025-05-12 12:20:28 +01:00
>
2025-05-19 12:28:38 +01:00
{roleLabels[role]}
2025-05-12 12:20:28 +01:00
</button>
))}
</div>
)}
</div>
{/* Sort */}
<div className="relative" ref={sortDropdownRef}>
<button
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
onClick={() => setSortDropdownOpen(v => !v)}
type="button"
>
Sort <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg>
</button>
{sortDropdownOpen && (
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
{sortFields.map(opt => (
<button
key={opt.value}
onClick={() => {
setSortField(opt.value);
setSortDropdownOpen(false);
}}
className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0
${sortField===opt.value ? "font-bold text-blue-600" : ""}`}
2025-05-12 12:20:28 +01:00
>
{opt.label}
</button>
))}
</div>
)}
</div>
{/* Asc/Desc Toggle */}
<button
className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
onClick={() => setSortDir(d => (d === "asc" ? "desc" : "asc"))}
title={sortDir === "asc" ? "Ascending" : "Descending"}
type="button"
>
{sortDir === "asc" ? "↑" : "↓"}
</button>
</div>
{/* Sort status text */}
<small className="text-xs text-gray-500 mb-2 px-1">
Users sorted by {fieldLabels[sortField]} {dirLabels[sortDir]}
</small>
{/* USERS LIST: full height, scrollable */}
<ul className="overflow-y-auto flex-1 pr-1">
{sortedUsers.map((user) => (
<li
key={user.email}
onClick={() => setSelectedEmail(user.email)}
className={`rounded-lg cursor-pointer border
${selectedEmail === user.email ? "bg-blue-100 border-blue-400" : "hover:bg-gray-200 border-transparent"}
transition px-2 py-1 mb-1`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium truncate">{user.name}</span>
2025-05-19 12:28:38 +01:00
<span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{roleLabels[user.role]}</span>
2025-05-12 12:20:28 +01:00
</div>
<div className="flex items-center justify-between mt-0.5">
<span className="text-xs text-gray-600 truncate">{user.email}</span>
</div>
</li>
))}
{sortedUsers.length === 0 && (
<li className="text-gray-400 text-center py-6">No users found.</li>
)}
</ul>
</div>
</div>
{/* MAIN PANEL */}
<div className="flex-1 p-10 bg-white overflow-y-auto">
{editUser ? (
<div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-bold mb-6">Edit User</h2>
<form className="space-y-4" onSubmit={handleUpdate}>
<div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account Creation Time:</label>
<span className="text-sm text-gray-500">{editUser.createdAt}</span>
</div>
2025-05-19 12:28:38 +01:00
<div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account ID Number:</label>
<span className="text-sm text-gray-500">{editUser.id}</span>
2025-05-19 12:28:38 +01:00
</div>
2025-05-12 12:20:28 +01:00
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">
Email (unique):
</label>
<input
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed"
type="email"
name="email"
value={editUser.email}
readOnly
onMouseEnter={() => setShowEmailTooltip(true)}
onMouseLeave={() => setShowEmailTooltip(false)}
2025-05-12 12:20:28 +01:00
/>
{/* Custom tooltip */}
{showEmailTooltip && (
<div className="absolute left-0 top-full mt-1 z-10 w-max max-w-xs bg-gray-800 text-gray-100 text-xs px-3 py-2 rounded-md shadow-lg border border-gray-700">
This field cannot be changed. <br />
To change the email, delete and re-add the user.
</div>
)}
2025-05-12 12:20:28 +01:00
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name:
</label>
<input
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
type="text"
name="name"
value={editUser.name}
onChange={handleEditChange}
2025-05-12 12:20:28 +01:00
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Role:
</label>
2025-05-12 12:20:28 +01:00
<select
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
name="role"
value={editUser.role}
onChange={handleEditChange}
2025-05-12 12:20:28 +01:00
>
{allRoles.map((role) => (
2025-05-19 12:28:38 +01:00
<option key={role} value={role}>{roleLabels[role]}</option>
2025-05-12 12:20:28 +01:00
))}
</select>
</div>
2025-05-19 12:28:38 +01:00
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password:
</label>
<input
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
type="text"
name="password"
value={editUser.password}
onChange={handleEditChange}
/>
2025-05-19 12:28:38 +01:00
</div>
2025-05-12 12:20:28 +01:00
<div className="flex gap-2 justify-end pt-6">
<button
type="button"
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition"
onClick={handleDelete}
>
Delete
</button>
<button
type="submit"
className={`px-4 py-2 rounded-lg font-semibold transition
${isEditChanged
? "bg-blue-600 hover:bg-blue-700 text-white shadow"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
disabled={!isEditChanged}
2025-05-12 12:20:28 +01:00
>
Update
</button>
</div>
</form>
</div>
) : (
<div className="text-center text-gray-400 mt-16 text-lg">
Select a user...
</div>
)}
</div>
</div>
</div>
);
}