Add user management
This commit is contained in:
40
src/components/auth/ForgotPasswordForm.tsx
Normal file
40
src/components/auth/ForgotPasswordForm.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
|
||||
async function onSubmit(formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
|
||||
await authClient.requestPasswordReset({
|
||||
email,
|
||||
// after user clicks email link, they'll land here:
|
||||
redirectTo: `${window.location.origin}/reset-password`,
|
||||
});
|
||||
|
||||
toast.success("If the email exists, a reset link has been sent.");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Request failed");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={onSubmit} className="space-y-3">
|
||||
<Input name="email" placeholder="Email" type="email" required />
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Sending…" : "Send reset link"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -1,11 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type ApiErrorShape =
|
||||
| { message?: string; error?: string; status?: string; code?: string }
|
||||
| null;
|
||||
|
||||
function isEmailNotVerified(data: ApiErrorShape) {
|
||||
const msg = (data?.message ?? data?.error ?? "").toLowerCase();
|
||||
const code = (data?.code ?? "").toLowerCase();
|
||||
const status = (data?.status ?? "").toLowerCase();
|
||||
|
||||
return (
|
||||
msg.includes("email not verified") ||
|
||||
code.includes("email_not_verified") ||
|
||||
(status.includes("forbidden") && msg.includes("verified"))
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginForm() {
|
||||
const router = useRouter();
|
||||
@ -15,30 +33,65 @@ export default function LoginForm() {
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [pending, setPending] = React.useState(false);
|
||||
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [needsVerification, setNeedsVerification] = React.useState(false);
|
||||
const [resendPending, setResendPending] = React.useState(false);
|
||||
|
||||
async function resendVerification() {
|
||||
setResendPending(true);
|
||||
try {
|
||||
// Endpoint name may differ slightly between versions,
|
||||
// but this is the common Better Auth route.
|
||||
const res = await fetch("/api/auth/send-verification-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
// Where user should land after verifying:
|
||||
callbackURL: `${window.location.origin}/`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = (await res.json().catch(() => null)) as ApiErrorShape;
|
||||
throw new Error(data?.message ?? "Failed to resend verification email.");
|
||||
}
|
||||
|
||||
toast.success("Verification email sent. Please check your inbox.");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed to resend verification email.");
|
||||
} finally {
|
||||
setResendPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setPending(true);
|
||||
setError(null);
|
||||
setNeedsVerification(false);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/auth/sign-in/email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null);
|
||||
const data = (await res.json().catch(() => null)) as ApiErrorShape;
|
||||
|
||||
if (isEmailNotVerified(data)) {
|
||||
setNeedsVerification(true);
|
||||
setError("Email not verified. Please verify your email to sign in.");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(data?.message ?? "Invalid email or password");
|
||||
return;
|
||||
}
|
||||
|
||||
// Successful login → redirect back
|
||||
router.replace(next);
|
||||
router.refresh();
|
||||
} catch {
|
||||
@ -59,6 +112,7 @@ export default function LoginForm() {
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={pending || resendPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -71,16 +125,38 @@ export default function LoginForm() {
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={pending || resendPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
{needsVerification && (
|
||||
<div className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Didn’t receive the verification email?
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={resendVerification}
|
||||
disabled={!email || resendPending || pending}
|
||||
>
|
||||
{resendPending ? "Sending…" : "Resend verification email"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={pending}>
|
||||
<Button type="submit" className="w-full" disabled={pending || resendPending}>
|
||||
{pending ? "Signing in…" : "Sign in"}
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<Link href="/forgot-password" className="underline underline-offset-4">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
51
src/components/auth/RegisterForm.tsx
Normal file
51
src/components/auth/RegisterForm.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { registerFirstUser } from "@/actions/auth/registerFirstUser";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function RegisterForm() {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
|
||||
async function onSubmit(formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const email = String(formData.get("email") ?? "").trim();
|
||||
|
||||
const res = await registerFirstUser({
|
||||
name: String(formData.get("name") ?? ""),
|
||||
email,
|
||||
password: String(formData.get("password") ?? ""),
|
||||
});
|
||||
|
||||
if (res.requiresEmailVerification) {
|
||||
toast.success("Account created. Please verify your email, then log in.");
|
||||
} else {
|
||||
toast.success("Account created.");
|
||||
}
|
||||
|
||||
router.replace("/login");
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Registration failed");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={onSubmit} className="space-y-3">
|
||||
<Input name="name" placeholder="Name" required />
|
||||
<Input name="email" placeholder="Email" type="email" required />
|
||||
<Input name="password" placeholder="Password" type="password" required />
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Creating…" : "Create account"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
49
src/components/auth/ResetPasswordForm.tsx
Normal file
49
src/components/auth/ResetPasswordForm.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function ResetPasswordForm({ token }: { token: string }) {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
|
||||
async function onSubmit(formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const password = String(formData.get("password") ?? "");
|
||||
const password2 = String(formData.get("password2") ?? "");
|
||||
|
||||
if (!token) throw new Error("Missing token.");
|
||||
if (password.length < 8) throw new Error("Password must be at least 8 characters.");
|
||||
if (password !== password2) throw new Error("Passwords do not match.");
|
||||
|
||||
await authClient.resetPassword({
|
||||
token,
|
||||
newPassword: password,
|
||||
});
|
||||
|
||||
toast.success("Password updated. You can now log in.");
|
||||
router.replace("/login");
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Reset failed");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={onSubmit} className="space-y-3">
|
||||
<Input name="password" placeholder="New password" type="password" required />
|
||||
<Input name="password2" placeholder="Repeat new password" type="password" required />
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating…" : "Update password"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -40,28 +40,16 @@ const commissionItems = [
|
||||
}
|
||||
]
|
||||
|
||||
// const portfolioItems = [
|
||||
// {
|
||||
// title: "Images",
|
||||
// href: "/portfolio/images",
|
||||
// },
|
||||
// {
|
||||
// title: "Types",
|
||||
// href: "/portfolio/types",
|
||||
// },
|
||||
// {
|
||||
// title: "Albums",
|
||||
// href: "/portfolio/albums",
|
||||
// },
|
||||
// {
|
||||
// title: "Categories",
|
||||
// href: "/portfolio/categories",
|
||||
// },
|
||||
// {
|
||||
// title: "Tags",
|
||||
// href: "/portfolio/tags",
|
||||
// },
|
||||
// ]
|
||||
const usersItems = [
|
||||
{
|
||||
title: "Users",
|
||||
href: "/users",
|
||||
},
|
||||
{
|
||||
title: "New User",
|
||||
href: "/users/new",
|
||||
}
|
||||
]
|
||||
|
||||
export default function TopNav() {
|
||||
return (
|
||||
@ -143,6 +131,25 @@ export default function TopNav() {
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Users</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-50 gap-4">
|
||||
{usersItems.map((item) => (
|
||||
<li key={item.title}>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href={item.href}>
|
||||
<div className="text-sm leading-none font-medium">{item.title}</div>
|
||||
<p className="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* <NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Portfolio</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
|
||||
60
src/components/users/CreateUserForm.tsx
Normal file
60
src/components/users/CreateUserForm.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { createUser } from "@/actions/users/createUser";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export function CreateUserForm() {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const [role, setRole] = React.useState<"user" | "admin">("user");
|
||||
|
||||
async function onSubmit(formData: FormData) {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await createUser({
|
||||
name: String(formData.get("name") ?? ""),
|
||||
email: String(formData.get("email") ?? ""),
|
||||
password: String(formData.get("password") ?? ""),
|
||||
role,
|
||||
});
|
||||
|
||||
toast.success("User created");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Create user failed");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={onSubmit} className="space-y-3">
|
||||
<Input name="name" placeholder="Name" required />
|
||||
<Input name="email" placeholder="Email" type="email" required />
|
||||
<Input name="password" placeholder="Password" type="password" required />
|
||||
|
||||
<Select value={role} onValueChange={(v) => setRole(v as any)}>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Creating…" : "Create user"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
232
src/components/users/UsersTable.tsx
Normal file
232
src/components/users/UsersTable.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import { MoreHorizontal, Trash2, UserPlus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { deleteUser } from "@/actions/users/deleteUser";
|
||||
import { getUsers, type UsersListRow } from "@/actions/users/getUsers";
|
||||
// import { resendVerification } from "@/actions/users/resendVerification";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
|
||||
function RoleBadge({ role }: { role: UsersListRow["role"] }) {
|
||||
return (
|
||||
<Badge variant={role === "admin" ? "default" : "secondary"} className="px-2 py-0.5">
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
function VerifiedBadge({ value }: { value: boolean }) {
|
||||
return (
|
||||
<Badge variant={value ? "default" : "secondary"} className="px-2 py-0.5">
|
||||
{value ? "Verified" : "Unverified"}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export function UsersTable() {
|
||||
const [rows, setRows] = React.useState<UsersListRow[]>([]);
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<{ id: string; label: string } | null>(null);
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
startTransition(async () => {
|
||||
const data = await getUsers();
|
||||
setRows(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{isPending ? "Updating…" : null} Total: {rows.length}
|
||||
</div>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/users/new">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Add user
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden rounded-2xl border border-border/60 bg-card shadow-sm ring-1 ring-border/40">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/40">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||
Name
|
||||
</TableHead>
|
||||
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||
Email
|
||||
</TableHead>
|
||||
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||
Role
|
||||
</TableHead>
|
||||
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="py-3 text-xs font-semibold uppercase tracking-wide text-foreground/80">
|
||||
Created
|
||||
</TableHead>
|
||||
<TableHead className="py-3" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="py-14 text-center">
|
||||
<div className="text-sm font-medium">{isPending ? "Loading…" : "No users."}</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((u, idx) => (
|
||||
<TableRow
|
||||
key={u.id}
|
||||
className={[
|
||||
"transition-colors",
|
||||
"hover:bg-muted/50",
|
||||
idx % 2 === 0 ? "bg-background" : "bg-muted/10",
|
||||
].join(" ")}
|
||||
>
|
||||
<TableCell className="py-3">
|
||||
<div className="text-sm font-medium">{u.name ?? "—"}</div>
|
||||
<div className="text-[11px] text-muted-foreground tabular-nums">{u.id}</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="py-3">
|
||||
<div className="text-sm">{u.email}</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="py-3">
|
||||
<RoleBadge role={u.role} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="py-3">
|
||||
<VerifiedBadge value={u.emailVerified} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="py-3">
|
||||
<span className="text-sm text-foreground/80">
|
||||
{new Date(u.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="py-3">
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Open row actions</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{/* Optional resend verification */}
|
||||
{/* {!u.emailVerified ? (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
startTransition(async () => {
|
||||
await resendVerification({ email: u.email });
|
||||
toast.success("Verification email resent");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Resend verification email
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
|
||||
{!u.emailVerified ? <DropdownMenuSeparator /> : null} */}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setDeleteTarget({ id: u.id, label: `${u.email}` });
|
||||
setDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete user?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will delete <span className="font-medium">{deleteTarget?.label}</span>. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={isPending || !deleteTarget}
|
||||
onClick={() => {
|
||||
const target = deleteTarget;
|
||||
if (!target) return;
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await deleteUser(target.id);
|
||||
toast.success("User deleted");
|
||||
setDeleteOpen(false);
|
||||
setDeleteTarget(null);
|
||||
refresh();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Delete failed");
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user