feat: Add loading states and improve form handling
This commit introduces several enhancements to form components and user experience across the application. - **Sign-in Form:** - Implemented a loading state using `useIsMutating` from TanStack Query. - The username and password fields are now disabled during the login process to prevent concurrent submissions. - The "Sign in" button displays a spinner while the mutation is pending, providing clear visual feedback. - **Assign to Company Modal:** - Refactored the modal to use `react-hook-form` for more robust state management and validation. - The form submission button is now disabled while the assignment request is in progress. - **Tag Input Component:** - Added a new `shouldAddWithSpace` prop to allow creating tags by pressing the space bar, increasing the component's flexibility. - **Header:** - Corrected the image path for the mobile header logo to display the correct icon.
This commit is contained in:
@@ -1,30 +1,32 @@
|
||||
"use client";
|
||||
import { PasswordIcon, UserIcon } from "@/assets/icons";
|
||||
|
||||
import InputGroup from "../FormElements/InputGroup";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { ILoginCredentials } from "@/lib/api";
|
||||
import { useLoginQuery } from "@/hooks/queries";
|
||||
import { useRef, useState } from "react";
|
||||
import { ILoginCredentials } from "@/lib/api";
|
||||
import { useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import InputGroup from "../FormElements/InputGroup";
|
||||
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { useIsMutating } from "@tanstack/react-query";
|
||||
|
||||
export default function SigninWithPassword() {
|
||||
const isMutating = useIsMutating();
|
||||
const [passwordFieldInputType, setPasswordFieldInputType] = useState<
|
||||
"password" | "text"
|
||||
>("password");
|
||||
const { handleSubmit, control, reset, register } =
|
||||
useForm<ILoginCredentials>();
|
||||
const { mutate, isPending } = useLoginQuery();
|
||||
const { mutate } = useLoginQuery(reset);
|
||||
|
||||
const onSubmit: SubmitHandler<ILoginCredentials> = (data) => {
|
||||
mutate(data);
|
||||
reset();
|
||||
};
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<InputGroup
|
||||
type="username"
|
||||
disabled={!!isMutating}
|
||||
{...register("username", {
|
||||
required: {
|
||||
value: true,
|
||||
@@ -40,6 +42,7 @@ export default function SigninWithPassword() {
|
||||
|
||||
<InputGroup
|
||||
type={passwordFieldInputType}
|
||||
disabled={!!isMutating}
|
||||
{...register("password", {
|
||||
required: {
|
||||
value: true,
|
||||
@@ -67,7 +70,11 @@ export default function SigninWithPassword() {
|
||||
type="submit"
|
||||
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary p-4 font-medium text-white transition hover:bg-opacity-90"
|
||||
>
|
||||
Sign In
|
||||
{isMutating ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
||||
) : (
|
||||
"Sign in"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -28,6 +28,7 @@ type TagInputProps<T extends FieldValues> = {
|
||||
watch?: UseFormWatch<T>;
|
||||
errors?: FieldErrors<T>;
|
||||
maxTags?: number;
|
||||
shouldAddWithSpace?: boolean;
|
||||
allowDuplicates?: boolean;
|
||||
tagValidator?: (tag: string) => boolean;
|
||||
} & Partial<UseFormRegisterReturn>;
|
||||
@@ -43,6 +44,7 @@ const TagInput = <T extends FieldValues>({
|
||||
name,
|
||||
register,
|
||||
setValue,
|
||||
shouldAddWithSpace = false,
|
||||
watch,
|
||||
errors,
|
||||
maxTags,
|
||||
@@ -87,7 +89,11 @@ const TagInput = <T extends FieldValues>({
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
if (
|
||||
e.key === "Enter" ||
|
||||
e.key === "," ||
|
||||
(shouldAddWithSpace && e.key === " ")
|
||||
) {
|
||||
e.preventDefault();
|
||||
addTag(inputValue);
|
||||
} else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
|
||||
@@ -167,7 +173,7 @@ const TagInput = <T extends FieldValues>({
|
||||
<input
|
||||
type="hidden"
|
||||
{...(register ? register(name) : {})}
|
||||
value={tags.length > 0 ? JSON.stringify(tags) : ''}
|
||||
value={tags.length > 0 ? JSON.stringify(tags) : ""}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function Header() {
|
||||
{isMobile && (
|
||||
<Link href={"/"} className="ml-2 max-[430px]:hidden min-[375px]:ml-4">
|
||||
<Image
|
||||
src={"/images/logo/logo-icon.svg"}
|
||||
src={"/images/fav-icon.svg"}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import {
|
||||
Dispatch,
|
||||
FC,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useState } from "react";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||
import { useCompanyFullList } from "@/hooks/queries";
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { Building } from "@/components/Layouts/sidebar/icons";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { useCompanyFullList } from "@/hooks/queries";
|
||||
import { TAssignCustomerToCompanyCredentials } from "@/lib/api";
|
||||
import { ILabelValue } from "@/types/shared";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
interface IProps {
|
||||
setCompanies: Dispatch<
|
||||
SetStateAction<TAssignCustomerToCompanyCredentials | null>
|
||||
SetStateAction<TAssignCustomerToCompanyCredentials | undefined>
|
||||
>;
|
||||
defaultValues?: TAssignCustomerToCompanyCredentials;
|
||||
}
|
||||
const AssignToCompanyModalContent: FC<IProps> = ({ setCompanies }) => {
|
||||
const AssignToCompanyModalContent: FC<IProps> = ({
|
||||
setCompanies,
|
||||
defaultValues,
|
||||
}) => {
|
||||
const [options, setOptions] = useState<ILabelValue[]>([]);
|
||||
|
||||
const { data, status, isSuccess } = useCompanyFullList();
|
||||
const { handleSubmit, register } =
|
||||
useForm<TAssignCustomerToCompanyCredentials>();
|
||||
useForm<TAssignCustomerToCompanyCredentials>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
|
||||
@@ -112,6 +112,9 @@ const CustomersTable = () => {
|
||||
onClick={() => {
|
||||
setCurrentCustomer(customer);
|
||||
setIsAssignCompanyModalOpen(true);
|
||||
setSelectedCompanies({
|
||||
company_ids: [customer?.companies?.[0]?.id ?? ""],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Assign To Company </span>
|
||||
@@ -145,7 +148,10 @@ const CustomersTable = () => {
|
||||
isOpen={isAssignCompanyModalOpen}
|
||||
onClose={() => setIsAssignCompanyModalOpen(false)}
|
||||
>
|
||||
<AssignToCompanyModalContent setCompanies={setSelectedCompanies} />
|
||||
<AssignToCompanyModalContent
|
||||
setCompanies={setSelectedCompanies}
|
||||
defaultValues={selectedCompanies}
|
||||
/>
|
||||
</Modal>
|
||||
{isModalOpen && (
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -12,8 +12,9 @@ import { useState } from "react";
|
||||
const useCustomerListPresenter = () => {
|
||||
const [isAssignCompanyModalOpen, setIsAssignCompanyModalOpen] =
|
||||
useState(false);
|
||||
const [selectedCompanies, setSelectedCompanies] =
|
||||
useState<TAssignCustomerToCompanyCredentials | null>(null);
|
||||
const [selectedCompanies, setSelectedCompanies] = useState<
|
||||
TAssignCustomerToCompanyCredentials | undefined
|
||||
>();
|
||||
const [currentCustomer, setCurrentCustomer] = useState<TCustomer | null>(
|
||||
null,
|
||||
);
|
||||
@@ -50,6 +51,7 @@ const useCustomerListPresenter = () => {
|
||||
deleteCustomer(currentCustomer.id);
|
||||
}
|
||||
};
|
||||
|
||||
const customers = data?.pages.flatMap((page) => page.data.customers) ?? [];
|
||||
return {
|
||||
allCustomers: customers,
|
||||
|
||||
@@ -497,6 +497,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
{...register("tags.cpv_codes")}
|
||||
label="CPV codes"
|
||||
name="tags.cpv_codes"
|
||||
shouldAddWithSpace
|
||||
watch={watch}
|
||||
setValue={setValue}
|
||||
placeholder="Enter Tag CPV codes"
|
||||
|
||||
@@ -6,6 +6,7 @@ import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
|
||||
import FormFooter from "@/components/ui/FormFooter";
|
||||
import { USERNAME_REGEX } from "@/constants/regex";
|
||||
import { FormErrorMessages } from "@/constants/Texts";
|
||||
import { CreateCustomerCredentials } from "@/lib/api";
|
||||
import useCreateCustomerPresenter from "./useCreateCustomerPresenter";
|
||||
@@ -36,7 +37,7 @@ const CreateCustomer = ({ defaultValues, editMode, id }: IProps) => {
|
||||
message: FormErrorMessages.maxLength(30),
|
||||
},
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/,
|
||||
value: USERNAME_REGEX,
|
||||
message: FormErrorMessages.invalidPattern,
|
||||
},
|
||||
})}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const PHONE_REGEX = /^[+\d]+$/;
|
||||
export const USERNAME_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9._]*[a-zA-Z0-9])?$/;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const useLoginQuery = () => {
|
||||
export const useLoginQuery = (callback?: () => void) => {
|
||||
const mutationKey = useMemo(() => [API_ENDPOINTS.USER.LOGIN], []);
|
||||
const { setAuthState } = useUser();
|
||||
const router = useRouter();
|
||||
@@ -18,9 +18,10 @@ export const useLoginQuery = () => {
|
||||
mutationKey,
|
||||
mutationFn: userService.login,
|
||||
onSuccess: (response) => {
|
||||
if (response.success) {
|
||||
setAuthState(response.data);
|
||||
router.replace("/charts");
|
||||
setAuthState(response.data);
|
||||
router.replace("/charts");
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user