Show success banner after sending a password-reset email
Deploy to route.crispygoat.com / deploy (push) Successful in 4m8s

The 'Send Reset Email' button in /admin/users previously just
cleared the error banner on success, with no actual 'yes it
worked' feedback. Added a green success banner that mirrors the
error banner's style and auto-dismisses after 6 seconds.

The 'Reset Password' button already shows confirmation in the
modal (temp password to copy, or 'reset email sent' message), so
it doesn't need the banner.

Also tightened the type narrowing in the resetAdminPassword unit
tests — the discriminated union needed a two-step
narrow (`r.success` then `r.method`) before TypeScript would
allow access to variant-specific fields like `tempPassword`.
This commit is contained in:
Tyler
2026-06-17 12:31:38 -06:00
parent eb37df347e
commit 7e079e0186
2 changed files with 39 additions and 9 deletions
+24 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import { AdminUserRow, UpdateAdminUserInput } from "@/actions/admin/users";
import { formatDate } from "@/lib/format-date";
import CreateUserModal from "./CreateUserModal";
@@ -124,6 +124,14 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
const [actionMenuId, setActionMenuId] = useState<string | null>(null);
const [passwordModal, setPasswordModal] = useState<PasswordModal | null>(null);
const [resetLinkLoading, setResetLinkLoading] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
// Auto-dismiss the success banner after 6 seconds.
useEffect(() => {
if (!success) return;
const t = setTimeout(() => setSuccess(null), 6000);
return () => clearTimeout(t);
}, [success]);
const canManageUsers =
currentUser.role === "platform_admin" ||
@@ -300,12 +308,14 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
async function handleSendResetLink(email: string) {
setResetLinkLoading(email);
setError(null);
setSuccess(null);
try {
const { sendPasswordResetEmail } = await import("@/actions/admin/users");
const result = await sendPasswordResetEmail(email);
setResetLinkLoading(null);
if (result.error) throw new Error(result.error);
setError(null);
setSuccess(`Password-reset email sent to ${email}.`);
} catch (e: unknown) {
setResetLinkLoading(null);
setError(e instanceof Error ? e.message : "Failed to send reset email");
@@ -341,6 +351,18 @@ export default function UsersPage({ initialUsers, brands, currentUser, onUserCre
)}
</div>
{/* Success banner */}
{success && (
<div className="mb-4 flex items-start justify-between rounded-lg bg-emerald-50 p-4 text-sm text-emerald-800 gap-3 border border-emerald-200">
<span> {success}</span>
<button onClick={() => setSuccess(null)} className="text-emerald-500 hover:text-emerald-700 shrink-0" aria-label="Dismiss">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
{/* Error banner */}
{error && (
<div className="mb-4 flex items-start justify-between rounded-lg bg-red-50 p-4 text-sm text-red-700 gap-3 border border-red-200">