diff --git a/public/locale/en.json b/public/locale/en.json
index 9aa895900a3..67706a9fe04 100644
--- a/public/locale/en.json
+++ b/public/locale/en.json
@@ -1286,8 +1286,7 @@
"never_logged_in": "Never Logged In",
"new_password": "New Password",
"new_password_confirmation": "Confirm New Password",
- "new_password_different_from_old": "Your new password is different from the old password.",
- "new_password_same_as_old": "Your new password must not match the old password.",
+ "new_password_same_as_old": "Your new password must not match the old password ",
"new_password_validation": "New password is not valid.",
"new_session": "New Session",
"next_month": "Next month",
@@ -1430,21 +1429,18 @@
"pain_chart_description": "Mark region and intensity of pain",
"passport_number": "Passport Number",
"password": "Password",
- "password_length_met": "It's at least 8 characters long",
- "password_length_validation": "Use at least 8 characters",
- "password_lowercase_met": "It includes at least one lowercase letter",
- "password_lowercase_validation": "Include at least one lowercase letter",
+ "password_length_validation": "Use at least 8 characters",
+ "password_lowercase_validation": "Include at least one lowercase letter (a-z)",
"password_mismatch": "Passwords do not match",
- "password_number_met": "It includes at least one number.",
- "password_number_validation": "Include at least one number.",
+ "password_number_validation": "Include at least one number (0-9)",
"password_required": "Password is required",
"password_reset_failure": "Password Reset Failed",
"password_reset_success": "Password Reset successfully",
"password_sent": "Password Reset Email Sent",
+ "password_success_message": "All set! Your password is strong",
"password_update_error": "Error while updating password. Try again later.",
"password_updated": "Password updated successfully",
- "password_uppercase_met": "It includes at least one uppercase letter.",
- "password_uppercase_validation": "Include at least one uppercase letter.",
+ "password_uppercase_validation": "Include at least one uppercase letter (A-Z).",
"passwords_match": "Passwords match.",
"patient": "Patient",
"patient-notes": "Notes",
@@ -2127,12 +2123,14 @@
"username": "Username",
"username_already_exists": "This username already exists",
"username_available": "Username is available",
- "username_characters_validation": "Only lowercase letters, numbers, and . _ - are allowed",
- "username_consecutive_validation": "Cannot contain consecutive special characters",
- "username_max_length_validation": "Use at most 16 characters",
- "username_min_length_validation": "Use at least 4 characters",
+ "username_characters_validation": "Only lowercase letters, numbers, and . _ - are allowed",
+ "username_consecutive_validation": "Cannot contain consecutive special characters",
+ "username_max_length_validation": "Use at most 16 characters",
+ "username_min_length_validation": "Use at least 4 characters",
"username_not_available": "Username is not available",
- "username_start_end_validation": "Must start and end with a letter or number",
+ "username_not_valid": "username is not valid",
+ "username_start_end_validation": "Must start and end with a letter or number",
+ "username_success_message": "All set! Your username is strong",
"username_userdetails_not_found": "Unable to fetch details as username or user details not found",
"username_valid": "Username is valid",
"users": "Users",
diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx
index 273e5486140..1f75e5a1ff6 100644
--- a/src/components/Auth/ResetPassword.tsx
+++ b/src/components/Auth/ResetPassword.tsx
@@ -6,7 +6,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { PasswordInput } from "@/components/ui/input-password";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import { ValidationHelper } from "@/components/Users/UserFormValidations";
import { LocalStorageKeys } from "@/common/constants";
import { validatePassword } from "@/common/validation";
@@ -27,9 +27,7 @@ const ResetPassword = (props: ResetPasswordProps) => {
const initErr: any = {};
const [form, setForm] = useState(initForm);
const [errors, setErrors] = useState(initErr);
- const [passwordInputInFocus, setPasswordInputInFocus] = useState(false);
- const [confirmPasswordInputInFocus, setConfirmPasswordInputInFocus] =
- useState(false);
+ const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);
const { t } = useTranslation();
const handleChange = (e: any) => {
@@ -124,40 +122,41 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="password"
placeholder={t("new_password")}
onChange={handleChange}
- onFocus={() => setPasswordInputInFocus(true)}
- onBlur={() => setPasswordInputInFocus(false)}
+ onFocus={() => setIsPasswordFieldFocused(true)}
+ onBlur={() => setIsPasswordFieldFocused(false)}
/>
{errors.password && (
{errors.password}
)}
- {passwordInputInFocus && (
-
- {validateRule(
- form.password?.length >= 8,
- t("password_length_validation"),
- !form.password,
- t("password_length_met"),
- )}
- {validateRule(
- form.password !== form.password.toUpperCase(),
- t("password_lowercase_validation"),
- !form.password,
- t("password_lowercase_met"),
- )}
- {validateRule(
- form.password !== form.password.toLowerCase(),
- t("password_uppercase_validation"),
- !form.password,
- t("password_uppercase_met"),
- )}
- {validateRule(
- /\d/.test(form.password),
- t("password_number_validation"),
- !form.password,
- t("password_number_met"),
- )}
+ {isPasswordFieldFocused && (
+
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(form.password),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(form.password),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(form.password),
+ },
+ ]}
+ />
)}
@@ -167,23 +166,12 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="confirm"
placeholder={t("confirm_password")}
onChange={handleChange}
- onFocus={() => setConfirmPasswordInputInFocus(true)}
- onBlur={() => setConfirmPasswordInputInFocus(false)}
/>
{errors.confirm && (
{errors.confirm}
)}
- {confirmPasswordInputInFocus &&
- form.confirm.length > 0 &&
- form.password.length > 0 &&
- validateRule(
- form.confirm === form.password,
- t("password_mismatch"),
- !form.password && form.password.length > 0,
- t("password_match"),
- )}
diff --git a/src/components/Users/UserForm.tsx b/src/components/Users/UserForm.tsx
index d94fd2dd731..9c7f517c9fb 100644
--- a/src/components/Users/UserForm.tsx
+++ b/src/components/Users/UserForm.tsx
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -29,7 +29,10 @@ import {
SelectValue,
} from "@/components/ui/select";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import {
+ ValidationHelper,
+ validateRule,
+} from "@/components/Users/UserFormValidations";
import { GENDER_TYPES } from "@/common/constants";
import { GENDERS } from "@/common/constants";
@@ -60,22 +63,22 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
? z.string().optional()
: z
.string()
- .min(4, t("username_min_length_validation"))
- .max(16, t("username_max_length_validation"))
- .regex(/^[a-z0-9._-]*$/, t("username_characters_validation"))
- .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_start_end_validation"))
+ .min(4, t("field_required"))
+ .max(16, t("username_not_valid"))
+ .regex(/^[a-z0-9._-]*$/, t("username_not_valid"))
+ .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_not_valid"))
.refine(
(val) => !val.match(/(?:[._-]{2,})/),
- t("username_consecutive_validation"),
+ t("username_not_valid"),
),
password: isEditMode
? z.string().optional()
: z
.string()
- .min(8, t("password_length_validation"))
- .regex(/[a-z]/, t("password_lowercase_validation"))
- .regex(/[A-Z]/, t("password_uppercase_validation"))
- .regex(/[0-9]/, t("password_number_validation")),
+ .min(8, t("field_required"))
+ .regex(/[a-z]/, t("new_password_validation"))
+ .regex(/[A-Z]/, t("new_password_validation"))
+ .regex(/[0-9]/, t("new_password_validation")),
c_password: isEditMode ? z.string().optional() : z.string(),
first_name: z.string().min(1, t("field_required")),
last_name: z.string().min(1, t("field_required")),
@@ -112,6 +115,12 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
resolver: zodResolver(userFormSchema),
defaultValues: {
user_type: "staff",
+ username: "",
+ password: "",
+ c_password: "",
+ first_name: "",
+ last_name: "",
+ email: "",
phone_number: "",
alt_phone_number: "",
phone_number_is_whatsapp: true,
@@ -125,7 +134,6 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
}),
enabled: !!existingUsername,
});
-
useEffect(() => {
if (userData && isEditMode) {
const formData: Partial = {
@@ -141,6 +149,9 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
}
}, [userData, form, isEditMode]);
+ const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);
+ const [isUsernameFieldFocused, setIsUsernameFieldFocused] = useState(false);
+
//const userType = form.watch("user_type");
const usernameInput = form.watch("username");
const phoneNumber = form.watch("phone_number");
@@ -171,12 +182,7 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
const isInitialRender = usernameInput === "";
if (username?.message) {
- return validateRule(
- false,
- username.message,
- isInitialRender,
- t("username_valid"),
- );
+ return null;
} else if (isUsernameChecking) {
return (
@@ -341,10 +347,57 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
data-cy="username-input"
placeholder={t("username")}
{...field}
+ onFocus={() => setIsUsernameFieldFocused(true)}
+ onBlur={() => setIsUsernameFieldFocused(false)}
/>
- {renderUsernameFeedback(usernameInput ?? "")}
+ {isUsernameFieldFocused ? (
+ <>
+
+ = 4,
+ },
+ {
+ description: "username_max_length_validation",
+ fulfilled: (field.value || "").length <= 16,
+ },
+ {
+ description: "username_characters_validation",
+ fulfilled: /^[a-z0-9._-]*$/.test(
+ field.value || "",
+ ),
+ },
+ {
+ description: "username_start_end_validation",
+ fulfilled: /^[a-z0-9].*[a-z0-9]$/.test(
+ field.value || "",
+ ),
+ },
+ {
+ description: "username_consecutive_validation",
+ fulfilled: !/(?:[._-]{2,})/.test(
+ field.value || "",
+ ),
+ },
+ ]}
+ />
+
+
+ {renderUsernameFeedback(usernameInput || "")}
+
+ >
+ ) : (
+
+ )}
)}
/>
@@ -361,9 +414,41 @@ export default function UserForm({ onSubmitSuccess, existingUsername }: Props) {
data-cy="password-input"
placeholder={t("password")}
{...field}
+ onFocus={() => setIsPasswordFieldFocused(true)}
+ onBlur={() => setIsPasswordFieldFocused(false)}
/>
-
+ {isPasswordFieldFocused ? (
+
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(field.value || ""),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(field.value || ""),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(field.value || ""),
+ },
+ ]}
+ />
+
+ ) : (
+
+ )}
)}
/>
diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx
index 4899a6f3d25..f07e08a9718 100644
--- a/src/components/Users/UserFormValidations.tsx
+++ b/src/components/Users/UserFormValidations.tsx
@@ -1,3 +1,5 @@
+import { Trans } from "react-i18next";
+
import CareIcon from "@/CAREUI/icons/CareIcon";
import { classNames } from "@/Utils/utils";
@@ -6,6 +8,54 @@ export type UserType = "doctor" | "nurse" | "staff" | "volunteer";
export type Gender = "male" | "female" | "non_binary" | "transgender";
+type Validation = {
+ description: string;
+ fulfilled: boolean;
+};
+
+type ValidationHelperProps = {
+ validations: Validation[];
+ successMessage: string;
+ isInputEmpty: boolean;
+};
+export const ValidationHelper = ({
+ validations,
+ successMessage,
+ isInputEmpty,
+}: ValidationHelperProps) => {
+ const unfulfilledValidations = validations.filter(
+ (validation) => !validation.fulfilled,
+ );
+
+ const allValid = unfulfilledValidations.length === 0 && !isInputEmpty;
+
+ return (
+
+ {isInputEmpty &&
+ validations.map((validation, index) => (
+
+
+
+ ))}
+ {!isInputEmpty &&
+ !allValid &&
+ unfulfilledValidations.map((validation, index) => (
+
+
+
+ ))}
+ {allValid && (
+ <>
+
+
+ {successMessage}
+
+ >
+ )}
+
+ );
+};
+
export const validateRule = (
isConditionMet: boolean,
initialMessage: JSX.Element | string,
diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx
index 6b0d343b0dc..fed51b5d5b3 100644
--- a/src/components/Users/UserResetPassword.tsx
+++ b/src/components/Users/UserResetPassword.tsx
@@ -19,7 +19,7 @@ import {
} from "@/components/ui/form";
import { PasswordInput } from "@/components/ui/input-password";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import { ValidationHelper } from "@/components/Users/UserFormValidations";
import { UpdatePasswordForm } from "@/components/Users/models";
import routes from "@/Utils/request/api";
@@ -152,36 +152,33 @@ export default function UserResetPassword({
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
- {validateRule(
- field.value.length >= 8,
- t("password_length_validation"),
- !field.value,
- t("password_length_met"),
- )}
- {validateRule(
- /[a-z]/.test(field.value),
- t("password_lowercase_validation"),
- !field.value,
- t("password_lowercase_met"),
- )}
- {validateRule(
- /[A-Z]/.test(field.value),
- t("password_uppercase_validation"),
- !field.value,
- t("password_uppercase_met"),
- )}
- {validateRule(
- /\d/.test(field.value),
- t("password_number_validation"),
- !field.value,
- t("password_number_met"),
- )}
- {validateRule(
- field.value !== form.watch("old_password"),
- t("new_password_same_as_old"),
- !field.value,
- t("new_password_different_from_old"),
- )}
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(field.value),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(field.value),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(field.value),
+ },
+ {
+ description: "new_password_same_as_old",
+ fulfilled:
+ field.value !== form.watch("old_password"),
+ },
+ ]}
+ />
) : (