Skip to content

Commit

Permalink
Add Real-Time Validation for CreateUserForm and Standardize UI Acr…
Browse files Browse the repository at this point in the history
…oss Forms (#10054)
  • Loading branch information
AdityaJ2305 authored Jan 30, 2025
1 parent 3976382 commit b949c64
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 108 deletions.
28 changes: 13 additions & 15 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1290,8 +1290,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 <strong>must not match the old password </strong> ",
"new_password_validation": "New password is not valid.",
"new_session": "New Session",
"next_month": "Next month",
Expand Down Expand Up @@ -1436,21 +1435,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 <strong>8 characters</strong>",
"password_lowercase_validation": "Include at least <strong>one lowercase letter</strong> (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 <strong>one number</strong> (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 <strong>one uppercase letter</strong> (A-Z).",
"passwords_match": "Passwords match.",
"patient": "Patient",
"patient-notes": "Notes",
Expand Down Expand Up @@ -2131,12 +2127,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 <strong>lowercase letters, numbers, and . _ - </strong>are allowed",
"username_consecutive_validation": "Cannot contain <strong>consecutive special characters</strong>",
"username_max_length_validation": "Use at most <strong>16 characters</strong>",
"username_min_length_validation": "Use at least <strong>4 characters</strong>",
"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 <strong>letter</strong> or <strong>number</strong>",
"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",
Expand Down
74 changes: 31 additions & 43 deletions src/components/Auth/ResetPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 && (
<div className="mt-1 text-red-500 text-xs" data-input-error>
{errors.password}
</div>
)}
{passwordInputInFocus && (
<div className="text-sm mt-2 pl-2 text-secondary-500">
{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 && (
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!form.password}
successMessage={t("password_success_message")}
validations={[
{
description: "password_length_validation",
fulfilled: form.password?.length >= 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),
},
]}
/>
</div>
)}
</div>
Expand All @@ -167,23 +166,12 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="confirm"
placeholder={t("confirm_password")}
onChange={handleChange}
onFocus={() => setConfirmPasswordInputInFocus(true)}
onBlur={() => setConfirmPasswordInputInFocus(false)}
/>
{errors.confirm && (
<div className="mt-1 text-red-500 text-xs" data-input-error>
{errors.confirm}
</div>
)}
{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"),
)}
</div>
</div>

Expand Down
124 changes: 105 additions & 19 deletions src/components/Users/UserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -68,22 +71,22 @@ export default function UserForm({
? 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")),
Expand Down Expand Up @@ -120,6 +123,12 @@ export default function UserForm({
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,
Expand All @@ -133,7 +142,6 @@ export default function UserForm({
}),
enabled: !!existingUsername,
});

useEffect(() => {
if (userData && isEditMode) {
const formData: Partial<UserFormValues> = {
Expand All @@ -149,6 +157,9 @@ export default function UserForm({
}
}, [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");
Expand Down Expand Up @@ -179,12 +190,7 @@ export default function UserForm({
const isInitialRender = usernameInput === "";

if (username?.message) {
return validateRule(
false,
username.message,
isInitialRender,
t("username_valid"),
);
return null;
} else if (isUsernameChecking) {
return (
<div className="flex items-center gap-1">
Expand Down Expand Up @@ -363,10 +369,57 @@ export default function UserForm({
data-cy="username-input"
placeholder={t("username")}
{...field}
onFocus={() => setIsUsernameFieldFocused(true)}
onBlur={() => setIsUsernameFieldFocused(false)}
/>
</div>
</FormControl>
{renderUsernameFeedback(usernameInput ?? "")}
{isUsernameFieldFocused ? (
<>
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!field.value}
successMessage={t("username_success_message")}
validations={[
{
description: "username_min_length_validation",
fulfilled: (field.value || "").length >= 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 || "",
),
},
]}
/>
</div>
<div className="pl-2">
{renderUsernameFeedback(usernameInput || "")}
</div>
</>
) : (
<FormMessage />
)}
</FormItem>
)}
/>
Expand All @@ -383,9 +436,41 @@ export default function UserForm({
data-cy="password-input"
placeholder={t("password")}
{...field}
onFocus={() => setIsPasswordFieldFocused(true)}
onBlur={() => setIsPasswordFieldFocused(false)}
/>
</FormControl>
<FormMessage />
{isPasswordFieldFocused ? (
<div
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
<ValidationHelper
isInputEmpty={!field.value}
successMessage={t("password_success_message")}
validations={[
{
description: "password_length_validation",
fulfilled: (field.value || "").length >= 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 || ""),
},
]}
/>
</div>
) : (
<FormMessage />
)}
</FormItem>
)}
/>
Expand Down Expand Up @@ -616,6 +701,7 @@ export default function UserForm({
type="submit"
className="w-full"
data-cy="submit-user-form"
variant="primary"
disabled={
isLoadingUser ||
!form.formState.isDirty ||
Expand Down
Loading

0 comments on commit b949c64

Please sign in to comment.