Organize/components/admin/admin-user-table.tsx

149 lines
5.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { toast } from "sonner";
import { KeyRound, Mail, MoreVertical, Trash2 } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog";
import { UserRoleMenu } from "@/components/admin/user-role-menu";
import { EditUserEmailDialog } from "@/components/admin/edit-user-email-dialog";
import { ChangePasswordDialog } from "@/components/admin/change-password-dialog";
import { deleteUser } from "@/lib/actions/admin";
import { Role } from "@/lib/generated/prisma/enums";
export interface AdminUserRow {
id: string;
name: string | null;
email: string;
role: Role;
createdAtLabel: string;
}
export function AdminUserTable({
initialUsers,
currentUserId,
}: {
initialUsers: AdminUserRow[];
currentUserId: string;
}) {
const [users, setUsers] = useState(initialUsers);
const [emailDialogFor, setEmailDialogFor] = useState<AdminUserRow | null>(null);
const [passwordDialogFor, setPasswordDialogFor] = useState<AdminUserRow | null>(null);
const [deleteDialogFor, setDeleteDialogFor] = useState<AdminUserRow | null>(null);
async function handleDelete(userId: string) {
const result = await deleteUser(userId);
if (result?.error) {
toast.error(result.error);
return;
}
setUsers((prev) => prev.filter((u) => u.id !== userId));
}
return (
<>
<div className="max-w-3xl overflow-x-auto rounded-xl border">
<table className="w-full min-w-[560px] text-sm">
<thead className="bg-muted/40 text-left text-muted-foreground">
<tr>
<th className="px-4 py-2 font-medium">Name</th>
<th className="px-4 py-2 font-medium">Email</th>
<th className="px-4 py-2 font-medium">Role</th>
<th className="px-4 py-2 font-medium">Joined</th>
<th className="px-4 py-2 font-medium">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="divide-y">
{users.map((user) => {
const isSelf = user.id === currentUserId;
return (
<tr key={user.id}>
<td className="px-4 py-2">{user.name || "—"}</td>
<td className="px-4 py-2 text-muted-foreground">{user.email}</td>
<td className="px-4 py-2">
<UserRoleMenu userId={user.id} role={user.role} />
</td>
<td className="px-4 py-2 text-muted-foreground">{user.createdAtLabel}</td>
<td className="px-4 py-2 text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
className="flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
aria-label={`More options for ${user.email}`}
>
<MoreVertical className="size-4" />
</button>
}
/>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setEmailDialogFor(user)}>
<Mail className="size-4" />
Edit email
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setPasswordDialogFor(user)}>
<KeyRound className="size-4" />
Change password
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={isSelf}
onClick={() => setDeleteDialogFor(user)}
>
<Trash2 className="size-4" />
Delete account
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{emailDialogFor && (
<EditUserEmailDialog
userId={emailDialogFor.id}
currentEmail={emailDialogFor.email}
open={!!emailDialogFor}
onOpenChange={(open) => !open && setEmailDialogFor(null)}
onSaved={(email) =>
setUsers((prev) => prev.map((u) => (u.id === emailDialogFor.id ? { ...u, email } : u)))
}
/>
)}
{passwordDialogFor && (
<ChangePasswordDialog
userId={passwordDialogFor.id}
userLabel={passwordDialogFor.email}
open={!!passwordDialogFor}
onOpenChange={(open) => !open && setPasswordDialogFor(null)}
/>
)}
{deleteDialogFor && (
<ConfirmDeleteDialog
open={!!deleteDialogFor}
onOpenChange={(open) => !open && setDeleteDialogFor(null)}
title="Delete account?"
description={`Delete ${deleteDialogFor.email}? This permanently deletes their account and all of their categories, groups, and to-dos. This can't be undone.`}
onConfirm={() => handleDelete(deleteDialogFor.id)}
/>
)}
</>
);
}