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"), + }, + ]} + /> ) : (