Auth Change Password
Auth Change Password is a React and Tailwind CSS text component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
text
What it is
Auth Change Password is a React and Tailwind CSS text component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Auth Change Password
"use client";
import { CheckCircle2, Eye, EyeOff, Loader2, Lock, Shield } from "lucide-react";
import { useCallback, useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/new-york/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york/ui/card";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
} from "@/registry/new-york/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/registry/new-york/ui/input-group";
export interface AuthChangePasswordProps {
onSubmit?: (data: { currentPassword: string; newPassword: string }) => void;
className?: string;
isLoading?: boolean;
isSuccess?: boolean;
errors?: {
currentPassword?: string;
newPassword?: string;
confirmPassword?: string;
general?: string;
};
successMessage?: string;
}
interface PasswordFieldProps {
id: string;
label: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error?: string;
showPassword: boolean;
onTogglePassword: () => void;
autoComplete: string;
placeholder: string;
icon: React.ReactNode;
description?: string;
}
function PasswordField({
id,
label,
value,
onChange,
error,
showPassword,
onTogglePassword,
autoComplete,
placeholder,
icon,
description,
}: PasswordFieldProps) {
const errorId = error ? `${id}-error` : undefined;
return (
<Field data-invalid={!!error}>
<FieldLabel htmlFor={id}>
{label}
<span aria-label="required" className="text-destructive">
*
</span>
</FieldLabel>
<FieldContent>
<InputGroup aria-invalid={!!error}>
<InputGroupAddon>{icon}</InputGroupAddon>
<InputGroupInput
aria-describedby={errorId}
aria-invalid={!!error}
autoComplete={autoComplete}
id={id}
name={id}
onChange={onChange}
placeholder={placeholder}
required
type={showPassword ? "text" : "password"}
value={value}
/>
<InputGroupButton
aria-label={
showPassword
? `Hide ${label.toLowerCase()}`
: `Show ${label.toLowerCase()}`
}
className="h-full min-w-[32px] touch-manipulation"
onClick={(e) => {
e.preventDefault();
onTogglePassword();
}}
type="button"
>
{showPassword ? (
<EyeOff aria-hidden="true" className="size-4" />
) : (
<Eye aria-hidden="true" className="size-4" />
)}
</InputGroupButton>
</InputGroup>
{error && (
<FieldError aria-live="polite" id={errorId}>
{error}
</FieldError>
)}
{description && <FieldDescription>{description}</FieldDescription>}
</FieldContent>
</Field>
);
}
interface SuccessStateProps {
message: string;
className?: string;
}
function SuccessState({ message, className }: SuccessStateProps) {
return (
<Card className={cn("w-full shadow-xs", className)}>
<CardHeader>
<CardTitle>Password changed</CardTitle>
<CardDescription>Your password has been updated</CardDescription>
</CardHeader>
<CardContent>
<div
aria-live="polite"
className="flex flex-col items-center gap-4 rounded-lg border border-primary/20 bg-primary/5 p-6 text-center"
role="status"
>
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10">
<CheckCircle2 aria-hidden="true" className="size-6 text-primary" />
</div>
<p className="font-medium text-sm">{message}</p>
</div>
</CardContent>
</Card>
);
}
interface ValidationErrors {
currentPassword?: string;
newPassword?: string;
confirmPassword?: string;
}
function validateCurrentPassword(value: string): string | undefined {
if (!value.trim()) {
return "Current password is required";
}
return;
}
function validateNewPassword(
value: string,
currentPassword: string
): string | undefined {
if (!value.trim()) {
return "New password is required";
}
if (value.length < 8) {
return "Password must be at least 8 characters";
}
if (!/(?=.*[a-z])/.test(value)) {
return "Password must contain at least one lowercase letter";
}
if (!/(?=.*[A-Z])/.test(value)) {
return "Password must contain at least one uppercase letter";
}
if (!/(?=.*\d)/.test(value)) {
return "Password must contain at least one number";
}
if (value === currentPassword) {
return "New password must be different from current password";
}
return;
}
function validateConfirmPassword(
value: string,
passwordValue: string
): string | undefined {
if (!value.trim()) {
return "Please confirm your password";
}
if (value !== passwordValue) {
return "Passwords do not match";
}
return;
}
export default function AuthChangePassword({
onSubmit,
className,
isLoading = false,
isSuccess = false,
errors,
successMessage = "Your password has been changed successfully.",
}: AuthChangePasswordProps) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [localErrors, setLocalErrors] = useState<ValidationErrors>({});
const handleCurrentPasswordChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setCurrentPassword(value);
if (localErrors.currentPassword) {
setLocalErrors((prev) => ({
...prev,
currentPassword: validateCurrentPassword(value),
}));
}
},
[localErrors.currentPassword]
);
const handleNewPasswordChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setNewPassword(value);
if (localErrors.newPassword) {
setLocalErrors((prev) => ({
...prev,
newPassword: validateNewPassword(value, currentPassword),
}));
}
if (localErrors.confirmPassword && confirmPassword) {
setLocalErrors((prev) => ({
...prev,
confirmPassword: validateConfirmPassword(confirmPassword, value),
}));
}
},
[
currentPassword,
confirmPassword,
localErrors.newPassword,
localErrors.confirmPassword,
]
);
const handleConfirmPasswordChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setConfirmPassword(value);
if (localErrors.confirmPassword) {
setLocalErrors((prev) => ({
...prev,
confirmPassword: validateConfirmPassword(value, newPassword),
}));
}
},
[newPassword, localErrors.confirmPassword]
);
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
const currentPasswordError = validateCurrentPassword(currentPassword);
const newPasswordError = validateNewPassword(
newPassword,
currentPassword
);
const confirmPasswordError = validateConfirmPassword(
confirmPassword,
newPassword
);
if (currentPasswordError || newPasswordError || confirmPasswordError) {
setLocalErrors({
currentPassword: currentPasswordError,
newPassword: newPasswordError,
confirmPassword: confirmPasswordError,
});
return;
}
setLocalErrors({});
onSubmit?.({
currentPassword: currentPassword.trim(),
newPassword: newPassword.trim(),
});
},
[currentPassword, newPassword, confirmPassword, onSubmit]
);
if (isSuccess) {
return <SuccessState className={className} message={successMessage} />;
}
const currentPasswordError =
errors?.currentPassword || localErrors.currentPassword;
const newPasswordError = errors?.newPassword || localErrors.newPassword;
const confirmPasswordError =
errors?.confirmPassword || localErrors.confirmPassword;
const generalError = errors?.general;
return (
<Card className={cn("w-full max-w-sm shadow-xs", className)}>
<CardHeader>
<CardTitle>Change password</CardTitle>
<CardDescription>
Update your password to keep your account secure
</CardDescription>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-6" onSubmit={handleSubmit}>
{generalError && (
<div
aria-live="polite"
className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm"
role="alert"
>
{generalError}
</div>
)}
<div className="flex flex-col gap-4">
<PasswordField
autoComplete="current-password"
error={currentPasswordError}
icon={<Lock aria-hidden="true" className="size-4" />}
id="change-current-password"
label="Current password"
onChange={handleCurrentPasswordChange}
onTogglePassword={() => setShowCurrentPassword((prev) => !prev)}
placeholder="Enter current password…"
showPassword={showCurrentPassword}
value={currentPassword}
/>
<PasswordField
autoComplete="new-password"
description="Must be at least 8 characters with uppercase, lowercase, and number"
error={newPasswordError}
icon={<Shield aria-hidden="true" className="size-4" />}
id="change-new-password"
label="New password"
onChange={handleNewPasswordChange}
onTogglePassword={() => setShowNewPassword((prev) => !prev)}
placeholder="Enter new password…"
showPassword={showNewPassword}
value={newPassword}
/>
<PasswordField
autoComplete="new-password"
error={confirmPasswordError}
icon={<Lock aria-hidden="true" className="size-4" />}
id="change-confirm-password"
label="Confirm new password"
onChange={handleConfirmPasswordChange}
onTogglePassword={() => setShowConfirmPassword((prev) => !prev)}
placeholder="Confirm new password…"
showPassword={showConfirmPassword}
value={confirmPassword}
/>
</div>
<Button
aria-busy={isLoading}
className="min-h-[44px] w-full touch-manipulation"
data-loading={isLoading}
disabled={isLoading}
type="submit"
>
{isLoading ? (
<>
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
Updating password…
</>
) : (
<>
<Shield aria-hidden="true" className="size-4" />
Update password
</>
)}
</Button>
</form>
</CardContent>
</Card>
);
}