feat: restyle membership page — filters, user table, modals
This commit is contained in:
@@ -31,8 +31,6 @@ export default async function MembershipPage({ searchParams }: MembershipPagePro
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<UserManagementTable initialData={userListData} roles={roles} currentUser={session.user!} />
|
||||||
<UserManagementTable initialData={userListData} roles={roles} currentUser={session.user!} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Role } from '@/lib/schemas';
|
import type { Role } from '@/lib/schemas';
|
||||||
import { createUserAction } from '@/lib/actions/membership';
|
import { createUserAction } from '@/lib/actions/membership';
|
||||||
|
import { GhButton } from '@/components/ui/GhButton';
|
||||||
|
|
||||||
interface CreateUserModalProps {
|
interface CreateUserModalProps {
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
@@ -44,17 +45,22 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||||
<div className="w-full max-w-md rounded-lg bg-gray-800 p-6">
|
<div className="w-full max-w-md rounded-lg border border-gh-border bg-gh-canvas p-6 shadow-xl">
|
||||||
<h3 className="mb-4 text-lg font-semibold text-white">Create New User</h3>
|
<h2 className="mb-4 text-base font-semibold text-gh-primary">Create New User</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 rounded-md bg-red-900 p-3 text-sm text-red-300">{error}</div>
|
<div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="create-username" className="block text-sm font-medium text-gray-300">
|
<label
|
||||||
|
htmlFor="create-username"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gh-secondary"
|
||||||
|
>
|
||||||
Username
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -64,12 +70,15 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
|
|||||||
onChange={(e) => setFormData((prev) => ({ ...prev, username: e.target.value }))}
|
onChange={(e) => setFormData((prev) => ({ ...prev, username: e.target.value }))}
|
||||||
required
|
required
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none disabled:opacity-50"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="create-password" className="block text-sm font-medium text-gray-300">
|
<label
|
||||||
|
htmlFor="create-password"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gh-secondary"
|
||||||
|
>
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -79,12 +88,15 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
|
|||||||
onChange={(e) => setFormData((prev) => ({ ...prev, password: e.target.value }))}
|
onChange={(e) => setFormData((prev) => ({ ...prev, password: e.target.value }))}
|
||||||
required
|
required
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none disabled:opacity-50"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label htmlFor="create-role" className="block text-sm font-medium text-gray-300">
|
<label
|
||||||
|
htmlFor="create-role"
|
||||||
|
className="mb-1.5 block text-sm font-medium text-gh-secondary"
|
||||||
|
>
|
||||||
Role
|
Role
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@@ -93,7 +105,7 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
|
|||||||
onChange={(e) => setFormData((prev) => ({ ...prev, role: e.target.value }))}
|
onChange={(e) => setFormData((prev) => ({ ...prev, role: e.target.value }))}
|
||||||
required
|
required
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none disabled:opacity-50"
|
className="form-input"
|
||||||
>
|
>
|
||||||
<option value="">Select a role...</option>
|
<option value="">Select a role...</option>
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
@@ -104,22 +116,13 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<GhButton type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</GhButton>
|
||||||
<button
|
<GhButton type="submit" variant="primary" disabled={isSubmitting}>
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="rounded-md bg-green-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-green-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Creating...' : 'Create User'}
|
{isSubmitting ? 'Creating...' : 'Create User'}
|
||||||
</button>
|
</GhButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { User } from '@/lib/schemas';
|
import type { User } from '@/lib/schemas';
|
||||||
import { deleteUserAction } from '@/lib/actions/membership';
|
import { deleteUserAction } from '@/lib/actions/membership';
|
||||||
|
import { GhButton } from '@/components/ui/GhButton';
|
||||||
|
|
||||||
interface DeleteUserModalProps {
|
interface DeleteUserModalProps {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -37,36 +38,29 @@ export function DeleteUserModal({ user, onClose }: DeleteUserModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||||||
<div className="w-full max-w-md rounded-lg bg-gray-800 p-6">
|
<div className="w-full max-w-md rounded-lg border border-gh-border bg-gh-canvas p-6 shadow-xl">
|
||||||
<h3 className="mb-4 text-lg font-semibold text-white">Delete User</h3>
|
<h2 className="mb-4 text-base font-semibold text-gh-primary">Delete User</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 rounded-md bg-red-900 p-3 text-sm text-red-300">{error}</div>
|
<div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="mb-6 text-gray-300">
|
<p className="mb-6 text-sm text-gh-secondary">
|
||||||
Are you sure you want to delete the user "{user.username}"? This action cannot
|
Are you sure you want to delete the user "{user.username}"? This action cannot
|
||||||
be undone.
|
be undone.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="flex justify-end space-x-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<GhButton type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</GhButton>
|
||||||
<button
|
<GhButton type="submit" variant="danger-solid" disabled={isSubmitting}>
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-red-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Deleting...' : 'Delete User'}
|
{isSubmitting ? 'Deleting...' : 'Delete User'}
|
||||||
</button>
|
</GhButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { Role, User } from '@/lib/schemas';
|
import type { Role, User } from '@/lib/schemas';
|
||||||
import { hasPermission } from '@/lib/schemas/user';
|
import { hasPermission } from '@/lib/schemas/user';
|
||||||
import { CreateUserModal } from './CreateUserModal';
|
import { CreateUserModal } from './CreateUserModal';
|
||||||
import { DeleteUserModal } from './DeleteUserModal';
|
import { DeleteUserModal } from './DeleteUserModal';
|
||||||
|
import { GhButton } from '@/components/ui/GhButton';
|
||||||
|
|
||||||
interface UserManagementTableProps {
|
interface UserManagementTableProps {
|
||||||
initialData: User[];
|
initialData: User[];
|
||||||
@@ -55,7 +55,7 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSortIcon = (field: string) => {
|
const getSortIcon = (field: string) => {
|
||||||
if (sortBy !== field) return '↕️';
|
if (sortBy !== field) return '↕';
|
||||||
return sortDesc ? '↓' : '↑';
|
return sortDesc ? '↓' : '↑';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,161 +79,137 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="bg-gray-800 shadow-md">
|
{/* Internal topbar */}
|
||||||
<div className="mx-auto flex max-w-[120rem] items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
<header className="flex h-12 items-center justify-between border-b border-gh-border-muted px-5">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/dashboard" className="text-gray-300 hover:text-white">
|
<span className="text-sm font-semibold text-gh-primary">User Management</span>
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-2xl font-bold">User Management</h1>
|
|
||||||
</div>
|
|
||||||
{hasPermission(currentUser, 'membership.create') && (
|
|
||||||
<button
|
|
||||||
onClick={openCreateModal}
|
|
||||||
className="rounded-md bg-green-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-green-700"
|
|
||||||
>
|
|
||||||
Create User
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{hasPermission(currentUser, 'membership.create') && (
|
||||||
|
<GhButton variant="primary" size="sm" onClick={openCreateModal}>
|
||||||
|
+ Create User
|
||||||
|
</GhButton>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="mx-auto max-w-[120rem] px-4 py-8 sm:px-6 lg:px-8">
|
{/* Filters */}
|
||||||
{/* Filters */}
|
<div className="mx-5 mt-4 rounded-lg border border-gh-border bg-gh-canvas p-4">
|
||||||
<div className="mb-6 rounded-lg border border-gray-700 bg-gray-800 p-4">
|
<div className="mb-3 text-sm font-semibold text-gh-primary">Filters</div>
|
||||||
<h2 className="mb-3 text-lg font-semibold">Filters</h2>
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div>
|
||||||
<div>
|
<label htmlFor="username" className="mb-1 block text-xs font-medium text-gh-muted">
|
||||||
<label htmlFor="username" className="block text-sm font-medium text-gray-300">
|
Username
|
||||||
Username
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
id="username"
|
||||||
id="username"
|
type="text"
|
||||||
type="text"
|
value={usernameFilter}
|
||||||
value={usernameFilter}
|
onChange={(e) => setUsernameFilter(e.target.value)}
|
||||||
onChange={(e) => setUsernameFilter(e.target.value)}
|
placeholder="Search by username..."
|
||||||
placeholder="Search by username..."
|
className="form-input"
|
||||||
className="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="role" className="block text-sm font-medium text-gray-300">
|
|
||||||
Role
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="role"
|
|
||||||
type="text"
|
|
||||||
value={roleNameFilter}
|
|
||||||
onChange={(e) => setRoleNameFilter(e.target.value)}
|
|
||||||
placeholder="Filter by role..."
|
|
||||||
className="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex space-x-2">
|
<div>
|
||||||
<button
|
<label htmlFor="role" className="mb-1 block text-xs font-medium text-gh-muted">
|
||||||
onClick={applyFilters}
|
Role
|
||||||
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-blue-700"
|
</label>
|
||||||
>
|
<input
|
||||||
Apply Filters
|
id="role"
|
||||||
</button>
|
type="text"
|
||||||
<button
|
value={roleNameFilter}
|
||||||
onClick={resetFilters}
|
onChange={(e) => setRoleNameFilter(e.target.value)}
|
||||||
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-700"
|
placeholder="Filter by role..."
|
||||||
>
|
className="form-input"
|
||||||
Reset
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-3 flex gap-2">
|
||||||
|
<GhButton variant="primary" size="sm" onClick={applyFilters}>
|
||||||
|
Apply
|
||||||
|
</GhButton>
|
||||||
|
<GhButton variant="ghost" size="sm" onClick={resetFilters}>
|
||||||
|
Reset
|
||||||
|
</GhButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Results Summary */}
|
{/* Results count */}
|
||||||
<div className="mb-4 text-sm text-gray-400">Showing {initialData.length} users</div>
|
<div className="mx-5 mt-3 text-xs text-gh-muted">Showing {initialData.length} users</div>
|
||||||
|
|
||||||
{/* Users Table */}
|
{/* Users table */}
|
||||||
<div className="overflow-hidden rounded-lg border border-gray-700 bg-gray-800">
|
<div className="mx-5 mt-2 mb-5 overflow-hidden rounded-lg border border-gh-border bg-gh-canvas">
|
||||||
<table className="min-w-full divide-y divide-gray-700">
|
<table className="min-w-full">
|
||||||
<thead className="bg-gray-900">
|
<thead>
|
||||||
<tr>
|
<tr className="border-b border-gh-border-muted bg-gh-base">
|
||||||
<th className="px-6 py-3 text-left">
|
<th className="px-4 py-2 text-left">
|
||||||
<button
|
<button
|
||||||
className="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase transition-colors hover:text-white"
|
onClick={() => handleSort('username')}
|
||||||
onClick={() => handleSort('username')}
|
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-gh-subtle hover:text-gh-primary"
|
||||||
>
|
>
|
||||||
<span>Username</span>
|
Username <span>{getSortIcon('username')}</span>
|
||||||
<span>{getSortIcon('username')}</span>
|
</button>
|
||||||
</button>
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('role')}
|
||||||
|
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-gh-subtle hover:text-gh-primary"
|
||||||
|
>
|
||||||
|
Role <span>{getSortIcon('role')}</span>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
{hasPermission(currentUser, 'membership.edit') && (
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
|
||||||
|
Actions
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left">
|
|
||||||
<button
|
|
||||||
className="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase transition-colors hover:text-white"
|
|
||||||
onClick={() => handleSort('role')}
|
|
||||||
>
|
|
||||||
<span>Role</span>
|
|
||||||
<span>{getSortIcon('role')}</span>
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
{hasPermission(currentUser, 'membership.edit') && (
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-700 bg-gray-800">
|
|
||||||
{initialData.length > 0 ? (
|
|
||||||
initialData.map((user) => (
|
|
||||||
<tr key={user.id} className="transition-colors hover:bg-gray-700">
|
|
||||||
<td className="px-6 py-4 text-sm font-medium whitespace-nowrap text-white">
|
|
||||||
{user.username}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm whitespace-nowrap">
|
|
||||||
<span className="inline-flex rounded-full bg-blue-900 px-2 py-1 text-xs font-semibold text-blue-300">
|
|
||||||
{user.role.name}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{hasPermission(currentUser, 'membership.edit') && (
|
|
||||||
<td className="px-6 py-4 text-sm font-medium whitespace-nowrap">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button className="text-blue-400 transition-colors hover:text-blue-300">
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
{hasPermission(currentUser, 'membership.delete') && (
|
|
||||||
<button
|
|
||||||
onClick={() => openDeleteModal(user)}
|
|
||||||
className="text-red-400 transition-colors hover:text-red-300"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} className="px-6 py-4 text-center text-sm text-gray-400">
|
|
||||||
No users found
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
</main>
|
{initialData.length > 0 ? (
|
||||||
|
initialData.map((user) => (
|
||||||
|
<tr
|
||||||
|
key={user.id}
|
||||||
|
className="border-b border-gh-border-muted transition-colors hover:bg-gh-hover last:border-0"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm font-medium text-gh-primary">
|
||||||
|
{user.username}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className="inline-flex rounded-full border border-gh-blue/20 bg-gh-blue-bg px-2 py-0.5 text-xs font-medium text-gh-blue">
|
||||||
|
{user.role.name}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{hasPermission(currentUser, 'membership.edit') && (
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button className="text-xs text-gh-blue hover:underline">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
{hasPermission(currentUser, 'membership.delete') && (
|
||||||
|
<button
|
||||||
|
onClick={() => openDeleteModal(user)}
|
||||||
|
className="text-xs text-gh-red hover:underline"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={hasPermission(currentUser, 'membership.edit') ? 3 : 2}
|
||||||
|
className="px-4 py-8 text-center text-sm text-gh-muted"
|
||||||
|
>
|
||||||
|
No users found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
{showCreateModal && <CreateUserModal roles={roles} onClose={closeCreateModal} />}
|
{showCreateModal && <CreateUserModal roles={roles} onClose={closeCreateModal} />}
|
||||||
|
|||||||
Reference in New Issue
Block a user