All components

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.

Open in the catalogue

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

registry/new-york/blocks/auth/auth-change-password.tsx
"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>
  );
}

Similar components