fix(projects): improve project access modal UI and UX
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { CheckSquare, Filter, ShieldCheck, Square } from "lucide-react";
|
||||
import {
|
||||
Briefcase,
|
||||
CheckCheck,
|
||||
CheckCircle2,
|
||||
CheckSquare,
|
||||
FolderTree,
|
||||
Loader2,
|
||||
Search,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Square,
|
||||
UserRound,
|
||||
Users,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
@@ -12,12 +26,14 @@ import { fetchWorkspaceMemberships, type WorkspaceMembership } from "../../api/w
|
||||
import { Modal } from "../Modal";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Select } from "../ui/Select";
|
||||
|
||||
type Labels = {
|
||||
title: string;
|
||||
description: string;
|
||||
close: string;
|
||||
member: string;
|
||||
projects: string;
|
||||
loading: string;
|
||||
noMembers: string;
|
||||
noProjects: string;
|
||||
@@ -67,19 +83,37 @@ export function ProjectAccessModal({
|
||||
const [loadingMembers, setLoadingMembers] = useState(false);
|
||||
const [selectedUserId, setSelectedUserId] = useState("");
|
||||
const [projectItems, setProjectItems] = useState<ProjectAccessItem[]>([]);
|
||||
const [selectedUserName, setSelectedUserName] = useState("");
|
||||
const [selectedUserMobile, setSelectedUserMobile] = useState("");
|
||||
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [memberSearchQuery, setMemberSearchQuery] = useState("");
|
||||
const [selectedClientId, setSelectedClientId] = useState("");
|
||||
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const isRtl =
|
||||
typeof document !== "undefined" && document.documentElement.dir === "rtl";
|
||||
|
||||
const manageableMembers = useMemo(
|
||||
() => members.filter((member) => member.is_active && MANAGEABLE_ROLES.has(member.role)),
|
||||
[members],
|
||||
);
|
||||
|
||||
const filteredMembers = useMemo(() => {
|
||||
const normalizedSearch = memberSearchQuery.trim().toLowerCase();
|
||||
const baseMembers = !normalizedSearch
|
||||
? manageableMembers
|
||||
: manageableMembers.filter((member) => {
|
||||
const memberName = getMemberName(member).toLowerCase();
|
||||
const memberMobile = member.user?.mobile?.toLowerCase() ?? "";
|
||||
return memberName.includes(normalizedSearch) || memberMobile.includes(normalizedSearch);
|
||||
});
|
||||
|
||||
return [...baseMembers].sort((a, b) => {
|
||||
if (a.user.id === selectedUserId) return -1;
|
||||
if (b.user.id === selectedUserId) return 1;
|
||||
return 0;
|
||||
});
|
||||
}, [manageableMembers, memberSearchQuery, selectedUserId]);
|
||||
|
||||
const clientOptions = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
projectItems.forEach((item) => {
|
||||
@@ -102,9 +136,17 @@ export function ProjectAccessModal({
|
||||
});
|
||||
}, [projectItems, searchQuery, selectedClientId]);
|
||||
|
||||
const visibleProjectIds = useMemo(() => visibleProjects.map((item) => item.id), [visibleProjects]);
|
||||
|
||||
const selectedVisibleCount = useMemo(
|
||||
() => selectedProjectIds.filter((id) => visibleProjectIds.includes(id)).length,
|
||||
[selectedProjectIds, visibleProjectIds],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSearchQuery("");
|
||||
setMemberSearchQuery("");
|
||||
setSelectedClientId("");
|
||||
setSelectedProjectIds([]);
|
||||
return;
|
||||
@@ -147,8 +189,6 @@ export function ProjectAccessModal({
|
||||
try {
|
||||
const response = await getProjectAccessState(workspaceId, selectedUserId);
|
||||
setProjectItems(response.items);
|
||||
setSelectedUserName(response.user.name);
|
||||
setSelectedUserMobile(response.user.mobile);
|
||||
setSelectedProjectIds([]);
|
||||
} catch {
|
||||
toast.error(labels.loadError);
|
||||
@@ -170,8 +210,7 @@ export function ProjectAccessModal({
|
||||
};
|
||||
|
||||
const handleSelectAllVisible = () => {
|
||||
const visibleIds = visibleProjects.map((item) => item.id);
|
||||
setSelectedProjectIds((current) => Array.from(new Set([...current, ...visibleIds])));
|
||||
setSelectedProjectIds((current) => Array.from(new Set([...current, ...visibleProjectIds])));
|
||||
};
|
||||
|
||||
const handleSelectClientProjects = () => {
|
||||
@@ -209,141 +248,353 @@ export function ProjectAccessModal({
|
||||
}
|
||||
};
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
<div className="inline-flex h-8 min-w-8 items-center justify-center rounded-full bg-slate-200 px-2 text-xs font-semibold text-slate-700 dark:bg-slate-700 dark:text-slate-200">
|
||||
{selectedProjectIds.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => void handleMutation("grant")}
|
||||
disabled={!selectedProjectIds.length || isSaving}
|
||||
className="gap-2"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ShieldCheck className="h-4 w-4" />}
|
||||
{labels.grantSelected}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => void handleMutation("revoke")}
|
||||
disabled={!selectedProjectIds.length || isSaving}
|
||||
className="gap-2"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ShieldAlert className="h-4 w-4" />}
|
||||
{labels.revokeSelected}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={labels.title}
|
||||
maxWidth="max-w-6xl"
|
||||
footer={
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{labels.close}
|
||||
</Button>
|
||||
}
|
||||
maxWidth="max-w-7xl"
|
||||
footer={footer}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{labels.description}</p>
|
||||
<p className="max-w-3xl text-sm text-slate-600 dark:text-slate-400">
|
||||
{labels.description}
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/60">
|
||||
<label className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{labels.member}
|
||||
</label>
|
||||
<select
|
||||
value={selectedUserId}
|
||||
onChange={(event) => setSelectedUserId(event.target.value)}
|
||||
className="h-11 w-full rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
|
||||
>
|
||||
{manageableMembers.map((member) => (
|
||||
<option key={member.id} value={member.user.id}>
|
||||
{getMemberName(member)} - {member.user.mobile}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="grid w-full grid-cols-1 gap-4 lg:grid-cols-12" dir="ltr">
|
||||
{/* LEFT SIDE */}
|
||||
<section
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
className="
|
||||
flex
|
||||
min-w-0
|
||||
min-h-[640px]
|
||||
flex-col
|
||||
overflow-hidden
|
||||
rounded-3xl
|
||||
border
|
||||
border-slate-200
|
||||
bg-slate-50/40
|
||||
dark:border-slate-800
|
||||
dark:bg-slate-950/30
|
||||
lg:col-span-8
|
||||
w-full
|
||||
"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="border-b border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/40">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
<Briefcase className="h-4 w-4" />
|
||||
{labels.projects}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-3 text-sm dark:border-slate-700 dark:bg-slate-900">
|
||||
<div className="font-medium text-slate-900 dark:text-slate-100">{selectedUserName || "-"}</div>
|
||||
<div className="mt-1 text-slate-500 dark:text-slate-400">{selectedUserMobile || "-"}</div>
|
||||
<div className="flex flex-col gap-3 xl:items-center">
|
||||
<div className="w-full relative min-w-0 flex-1">
|
||||
<Search className="absolute left-3 rtl:left-auto rtl:right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
||||
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder={labels.searchPlaceholder}
|
||||
className="w-full pl-10 pr-4 rtl:pl-4 rtl:pr-10 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-blue-500 transition-shadow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={selectedClientId}
|
||||
onChange={setSelectedClientId}
|
||||
options={[
|
||||
{ value: "", label: labels.allClients },
|
||||
...clientOptions.map((client) => ({
|
||||
value: client.id,
|
||||
label: client.name,
|
||||
})),
|
||||
]}
|
||||
className="w-full"
|
||||
buttonClassName="h-11 w-full rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingMembers ? (
|
||||
<div className="mt-4 text-sm text-slate-500 dark:text-slate-400">{labels.loading}</div>
|
||||
) : manageableMembers.length === 0 ? (
|
||||
<div className="mt-4 text-sm text-slate-500 dark:text-slate-400">{labels.noMembers}</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder={labels.searchPlaceholder}
|
||||
/>
|
||||
<select
|
||||
value={selectedClientId}
|
||||
onChange={(event) => setSelectedClientId(event.target.value)}
|
||||
className="h-11 rounded-xl border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none transition focus:border-sky-400 focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-900">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={handleSelectAllVisible}
|
||||
disabled={!visibleProjects.length}
|
||||
title={labels.selectAllVisible}
|
||||
aria-label={labels.selectAllVisible}
|
||||
>
|
||||
<option value="">{labels.allClients}</option>
|
||||
{clientOptions.map((client) => (
|
||||
<option key={client.id} value={client.id}>
|
||||
{client.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => setSelectedProjectIds([])}
|
||||
disabled={!selectedProjectIds.length}
|
||||
title={labels.clearSelection}
|
||||
aria-label={labels.clearSelection}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={handleSelectClientProjects}
|
||||
disabled={!selectedClientId}
|
||||
title={labels.selectClientProjects}
|
||||
aria-label={labels.selectClientProjects}
|
||||
>
|
||||
<FolderTree className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="ms-auto text-xs font-medium text-slate-500 dark:text-slate-400">
|
||||
{selectedVisibleCount}/{visibleProjects.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="secondary" onClick={handleSelectAllVisible} disabled={!visibleProjects.length}>
|
||||
<CheckSquare className="me-2 h-4 w-4" />
|
||||
{labels.selectAllVisible}
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => setSelectedProjectIds([])} disabled={!selectedProjectIds.length}>
|
||||
<Square className="me-2 h-4 w-4" />
|
||||
{labels.clearSelection}
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" onClick={handleSelectClientProjects} disabled={!selectedClientId}>
|
||||
<Filter className="me-2 h-4 w-4" />
|
||||
{labels.selectClientProjects}
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void handleMutation("grant")} disabled={!selectedProjectIds.length || isSaving}>
|
||||
{labels.grantSelected}
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" onClick={() => void handleMutation("revoke")} disabled={!selectedProjectIds.length || isSaving}>
|
||||
{labels.revokeSelected}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="max-h-[480px] overflow-y-auto">
|
||||
{/* Projects */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<div className="h-full">
|
||||
{loadingProjects ? (
|
||||
<div className="p-4 text-sm text-slate-500 dark:text-slate-400">{labels.loading}</div>
|
||||
<div className="flex items-center gap-2 p-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{labels.loading}
|
||||
</div>
|
||||
) : visibleProjects.length === 0 ? (
|
||||
<div className="p-4 text-sm text-slate-500 dark:text-slate-400">{labels.noProjects}</div>
|
||||
<div className="p-5 text-sm text-slate-500 dark:text-slate-400">
|
||||
{labels.noProjects}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-200 dark:divide-slate-800">
|
||||
<div className="grid gap-3">
|
||||
{visibleProjects.map((item) => {
|
||||
const isChecked = selectedProjectIds.includes(item.id);
|
||||
|
||||
return (
|
||||
<label
|
||||
<button
|
||||
key={item.id}
|
||||
className="flex cursor-pointer items-start gap-3 px-4 py-3 transition hover:bg-slate-50 dark:hover:bg-slate-800/60"
|
||||
type="button"
|
||||
onClick={() => toggleProjectSelection(item.id)}
|
||||
className={`flex w-full items-start gap-3 rounded-2xl border px-4 py-3 text-start transition ${
|
||||
isChecked
|
||||
? "border-sky-200 bg-sky-50/60 shadow-sm dark:border-sky-500/30 dark:bg-sky-500/10"
|
||||
: "border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50/70 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700 dark:hover:bg-slate-800/40"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => toggleProjectSelection(item.id)}
|
||||
className="mt-1 h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
{/* Checkbox */}
|
||||
<div
|
||||
className={`mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition ${
|
||||
isChecked
|
||||
? "border-sky-500 bg-sky-500 text-white"
|
||||
: "border-slate-300 bg-white text-slate-400 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-500"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{isChecked ? (
|
||||
<CheckSquare className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Square className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-slate-900 dark:text-slate-100">{item.name}</span>
|
||||
<span className="truncate font-medium text-slate-900 dark:text-slate-100">
|
||||
{item.name}
|
||||
</span>
|
||||
|
||||
{/* Better dark mode badge */}
|
||||
<span
|
||||
className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${
|
||||
className={`inline-flex items-center rounded-full px-2 py-1 text-[11px] font-medium ${
|
||||
item.has_access
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
|
||||
: "bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
||||
? `
|
||||
bg-emerald-100 text-emerald-700
|
||||
dark:bg-emerald-500/15
|
||||
dark:text-emerald-200
|
||||
dark:ring-1 dark:ring-emerald-400/25
|
||||
`
|
||||
: `
|
||||
bg-slate-100 text-slate-600
|
||||
dark:bg-slate-800
|
||||
dark:text-slate-200
|
||||
dark:ring-1 dark:ring-slate-700
|
||||
`
|
||||
}`}
|
||||
>
|
||||
{item.has_access ? labels.accessOn : labels.accessOff}
|
||||
{item.has_access
|
||||
? labels.accessOn
|
||||
: labels.accessOff}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{labels.client}: {item.client?.name || labels.noClient}
|
||||
{item.client?.name || labels.noClient}
|
||||
</div>
|
||||
|
||||
{item.description ? (
|
||||
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">{item.description}</div>
|
||||
<div className="mt-1 line-clamp-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{item.description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</label>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* RIGHT SIDEBAR */}
|
||||
<aside
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
className="
|
||||
flex
|
||||
min-w-0
|
||||
min-h-[640px]
|
||||
flex-col
|
||||
overflow-hidden
|
||||
rounded-3xl
|
||||
border
|
||||
border-slate-200
|
||||
bg-white
|
||||
dark:border-slate-800
|
||||
dark:bg-slate-900
|
||||
lg:col-span-4
|
||||
lg:min-w-[320px]
|
||||
w-full
|
||||
"
|
||||
>
|
||||
{/* Sidebar Header */}
|
||||
<div className="border-b border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/40">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
<Users className="h-4 w-4" />
|
||||
{labels.member}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 rtl:left-auto rtl:right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
||||
|
||||
<Input
|
||||
value={memberSearchQuery}
|
||||
onChange={(event) =>
|
||||
setMemberSearchQuery(event.target.value)
|
||||
}
|
||||
placeholder={labels.member}
|
||||
className="w-full pl-10 pr-4 rtl:pl-4 rtl:pr-10 py-2.5 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white outline-none focus:ring-2 focus:ring-blue-500 transition-shadow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Users List */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
<div className="grid gap-2">
|
||||
{loadingMembers ? (
|
||||
<div className="flex items-center gap-2 p-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{labels.loading}
|
||||
</div>
|
||||
) : filteredMembers.length === 0 ? (
|
||||
<div className="p-4 text-sm text-slate-500 dark:text-slate-400">
|
||||
{labels.noMembers}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{filteredMembers.map((member) => {
|
||||
const isActive =
|
||||
member.user.id === selectedUserId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={member.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedUserId(member.user.id)
|
||||
}
|
||||
className={`flex w-full items-start gap-3 rounded-2xl px-4 py-3 text-start transition ${
|
||||
isActive
|
||||
? "bg-sky-50/80 dark:bg-sky-500/10"
|
||||
: "hover:bg-slate-50/70 dark:hover:bg-slate-800/40"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
|
||||
isActive
|
||||
? "bg-sky-500 text-white"
|
||||
: "bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-300"
|
||||
}`}
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="truncate font-medium text-slate-900 dark:text-slate-100">
|
||||
{getMemberName(member)}
|
||||
</div>
|
||||
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2 py-1 text-[11px] font-medium capitalize text-slate-600 dark:bg-slate-800 dark:text-slate-300">
|
||||
{member.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isActive ? (
|
||||
<CheckCircle2 className="h-4 w-4 shrink-0 text-sky-500 dark:text-sky-300" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{member.user.mobile}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user