complete auth pages sunc with api - complete cities and province combo box in register page

This commit is contained in:
Alihaghighattalab
2024-08-24 16:42:35 +03:30
parent ea510ee0bf
commit 9a63ef4e9e
14 changed files with 404 additions and 102 deletions
+5 -3
View File
@@ -7,12 +7,14 @@ type Props = {
type?: "reset" | "button" | "submit" | undefined, type?: "reset" | "button" | "submit" | undefined,
pending?: boolean, pending?: boolean,
theme?: "primary" | "secondary" | "glass" | "light", theme?: "primary" | "secondary" | "glass" | "light",
onClick?: () => void onClick?: () => void,
disable?: boolean,
className?: string
} }
export const ButtonComponent: FC<Props> = ({ title, type = "button", pending = false, theme = "primary", onClick }) => { export const ButtonComponent: FC<Props> = ({ title, type = "button", pending = false, theme = "primary", onClick, disable = false, className }) => {
return ( return (
<Button type={type} onClick={onClick} className={clsx("flex flex-row gap-3 items-center justify-center", { <Button type={type} disabled={disable} onClick={onClick} className={clsx(`flex flex-row gap-3 items-center justify-center ${className}`, {
"bg-primary-color text-white": theme === "primary", "bg-primary-color text-white": theme === "primary",
"bg-transparent text-primary-color": theme === "glass", "bg-transparent text-primary-color": theme === "glass",
"bg-dark-blue text-white": theme === "secondary", "bg-dark-blue text-white": theme === "secondary",
+1 -5
View File
@@ -25,7 +25,6 @@ export const ComboBox: FC<Props> = ({ field, placeholder, className, data = [],
return ( return (
<Combobox value={field && field.value} onChange={mode === "default" ? field && field.onChange : (e: ComboBoxItems) => { <Combobox value={field && field.value} onChange={mode === "default" ? field && field.onChange : (e: ComboBoxItems) => {
console.log(e)
updateParams({ updateParams({
rows: e?.id rows: e?.id
}) })
@@ -36,9 +35,6 @@ export const ComboBox: FC<Props> = ({ field, placeholder, className, data = [],
className={`input-style min-w-full ${className}`} className={`input-style min-w-full ${className}`}
displayValue={(person: any) => person?.name} displayValue={(person: any) => person?.name}
onChange={(event) => setQuery(event.target.value)} onChange={(event) => setQuery(event.target.value)}
ref={field && field.ref}
name={field && field.name}
onBlur={field && field.onBlur}
/> />
<ComboboxButton className="absolute top-0 min-w-full h-full bg-transparent"> <ComboboxButton className="absolute top-0 min-w-full h-full bg-transparent">
<ArrowDown2 className="size-5 absolute left-5 top-5 p-0 text-secondary-text-color/70" /> <ArrowDown2 className="size-5 absolute left-5 top-5 p-0 text-secondary-text-color/70" />
@@ -47,7 +43,7 @@ export const ComboBox: FC<Props> = ({ field, placeholder, className, data = [],
{data?.length > 0 && <ComboboxOptions {data?.length > 0 && <ComboboxOptions
anchor="bottom start" anchor="bottom start"
transition transition
className="bg-white shadow min-w-[220px] mt-2 rounded-xl p-2" className="bg-white shadow min-w-[220px] !max-h-[300px] mt-2 rounded-xl p-2"
> >
{filteredValue.map((value) => ( {filteredValue.map((value) => (
<ComboboxOption <ComboboxOption
+30 -6
View File
@@ -1,13 +1,22 @@
import { Link } from "react-router-dom" import { Link, useNavigate } from "react-router-dom"
import { Input } from "../common/input" import { Input } from "../common/input"
import { Controller, SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { ForgotPasswordInterface } from "../../types" import { ForgotPasswordInterface } from "../../types"
import { yupResolver } from "@hookform/resolvers/yup" import { yupResolver } from "@hookform/resolvers/yup"
import { forgotPasswordSchema } from "../../schema" import { forgotPasswordSchema } from "../../schema"
import { ErrorComponent } from "../common/error" import { ErrorComponent } from "../common/error"
import { Button } from "@headlessui/react" import { useMutation } from "@tanstack/react-query"
import { sendOTPMutation } from "../../services/api/auth"
import toast from "react-hot-toast"
import { ButtonComponent } from "../common/button"
export const ForgotPasswordForm = () => { export const ForgotPasswordForm = () => {
const navigate = useNavigate()
const { mutate, isPending } = useMutation({
mutationFn: sendOTPMutation,
mutationKey: ["send-otp"]
})
const { const {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
@@ -15,7 +24,22 @@ export const ForgotPasswordForm = () => {
} = useForm<ForgotPasswordInterface>({ } = useForm<ForgotPasswordInterface>({
resolver: yupResolver(forgotPasswordSchema) resolver: yupResolver(forgotPasswordSchema)
}) })
const handleForgotSubmit: SubmitHandler<ForgotPasswordInterface> = async (data) => { console.log("data=>", data) } const handleForgotSubmit: SubmitHandler<ForgotPasswordInterface> = async (data) => {
mutate({
...data
},
{
onSuccess: () => {
toast.success(`کد ورود برای شماره ${data?.mobile_number} ارسال شد`)
navigate(`/auth/reset-password-code?phone_number=${data?.mobile_number}`)
},
onError: (err: any) => {
console.log("error", err)
navigate(`/auth/reset-password-code?phone_number=${data?.mobile_number}`)
}
}
)
}
return ( return (
<form onSubmit={handleSubmit(handleForgotSubmit)} <form onSubmit={handleSubmit(handleForgotSubmit)}
@@ -29,15 +53,15 @@ export const ForgotPasswordForm = () => {
</div> </div>
</div> </div>
<div className="mt-9 mb-[42px]"> <div className="mt-9 mb-[42px]">
<Controller control={control} name="phoneNumber" render={({ field }) => ( <Controller control={control} name="mobile_number" render={({ field }) => (
<div> <div>
<Input type="number" placeholder="شماره موبایل" field={field} /> <Input type="number" placeholder="شماره موبایل" field={field} />
<ErrorComponent show={errors.phoneNumber} message={errors?.phoneNumber?.message} /> <ErrorComponent show={errors.mobile_number} message={errors?.mobile_number?.message} />
</div> </div>
)} /> )} />
</div> </div>
<div className="flex flex-col gap-y-[27px] items-center"> <div className="flex flex-col gap-y-[27px] items-center">
<Button type="submit">دریافت کد تایید</Button> <ButtonComponent title="دریافت کد تایید" type="submit" pending={isPending} />
<Link to="/auth/login"> <Link to="/auth/login">
<p className="font-medium text-base text-primary-color">بازگشت</p> <p className="font-medium text-base text-primary-color">بازگشت</p>
</Link> </Link>
+27 -6
View File
@@ -1,18 +1,39 @@
import { Link } from "react-router-dom" import { Link, useNavigate } from "react-router-dom"
import { Input } from "../common/input" import { Input } from "../common/input"
import { Controller, SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { LoginWithCodeInterface } from "../../types" import { LoginWithCodeInterface } from "../../types"
import { yupResolver } from "@hookform/resolvers/yup" import { yupResolver } from "@hookform/resolvers/yup"
import { LoginWithCodeSchema } from "../../schema" import { LoginWithCodeSchema } from "../../schema"
import { ErrorComponent } from "../common/error" import { ErrorComponent } from "../common/error"
import { Button } from "@headlessui/react" import { ButtonComponent } from "../common/button"
import { useMutation } from "@tanstack/react-query"
import { sendOTPMutation } from "../../services/api/auth"
import toast from "react-hot-toast"
export const LoginWithCodeForm = () => { export const LoginWithCodeForm = () => {
const navigate = useNavigate()
const { mutate, isPending } = useMutation({
mutationFn: sendOTPMutation,
mutationKey: ["send-otp"]
})
const { handleSubmit, formState: { errors }, control } = useForm<LoginWithCodeInterface>({ const { handleSubmit, formState: { errors }, control } = useForm<LoginWithCodeInterface>({
resolver: yupResolver(LoginWithCodeSchema) resolver: yupResolver(LoginWithCodeSchema)
}) })
const handleLoginWithCode: SubmitHandler<LoginWithCodeInterface> = async (data) => { const handleLoginWithCode: SubmitHandler<LoginWithCodeInterface> = async (data) => {
console.log("data=>", data) mutate({
...data
},
{
onSuccess: () => {
toast.success(`کد ورود برای شماره ${data?.mobile_number} ارسال شد`)
navigate(`/auth/send-code?phone_number=${data?.mobile_number}`)
},
onError: (err: any) => {
console.log("error", err)
navigate(`/auth/send-code?phone_number=${data?.mobile_number}`)
}
}
)
} }
return ( return (
<form onSubmit={handleSubmit(handleLoginWithCode)} <form onSubmit={handleSubmit(handleLoginWithCode)}
@@ -26,15 +47,15 @@ export const LoginWithCodeForm = () => {
</div> </div>
</div> </div>
<div className="mt-9 mb-[42px]"> <div className="mt-9 mb-[42px]">
<Controller control={control} name="phoneNumber" render={({ field }) => ( <Controller control={control} name="mobile_number" render={({ field }) => (
<div> <div>
<Input type="number" placeholder="شماره موبایل" field={field} /> <Input type="number" placeholder="شماره موبایل" field={field} />
<ErrorComponent show={errors.phoneNumber} message={errors.phoneNumber?.message} /> <ErrorComponent show={errors.mobile_number} message={errors.mobile_number?.message} />
</div> </div>
)} /> )} />
</div> </div>
<div className="flex flex-col gap-y-[27px] items-center"> <div className="flex flex-col gap-y-[27px] items-center">
<Button type="submit">دریافت کد تایید</Button> <ButtonComponent title="دریافت کد تایید" pending={isPending} type="submit" />
<Link to="/auth/login"> <Link to="/auth/login">
<p className="font-medium text-base text-primary-color">بازگشت</p> <p className="font-medium text-base text-primary-color">بازگشت</p>
</Link> </Link>
+86 -27
View File
@@ -1,20 +1,75 @@
import { Link } from "react-router-dom" import { Link, useNavigate } from "react-router-dom"
import { ComboBox } from "../common/combo-box" import { ComboBox } from "../common/combo-box"
import { Input } from "../common/input" import { Input } from "../common/input"
import { Button } from "@headlessui/react"
import { Controller, SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { RegisterInterface } from "../../types" import { CitiesType, ComboBoxItems, ProvinceType, RegisterInterface } from "../../types"
import { yupResolver } from "@hookform/resolvers/yup" import { yupResolver } from "@hookform/resolvers/yup"
import { RegisterSchema } from "../../schema" import { RegisterSchema } from "../../schema"
import { ErrorComponent } from "../common/error" import { ErrorComponent } from "../common/error"
import { getProvincesList } from "../../services/api/province"
import { useMutation, useQuery } from "@tanstack/react-query"
import { useEffect, useState } from "react"
import { getCitiesList } from "../../services/api/cities"
import { registerMutation } from "../../services/api/auth"
import { ButtonComponent } from "../common/button"
import toast from "react-hot-toast"
export const RegisterForm = () => { export const RegisterForm = () => {
const { handleSubmit, formState: { errors }, control } = useForm<RegisterInterface>({ const [provinceData, setProvinceData] = useState<ComboBoxItems[] | []>([])
const [citiesData, setCitiesData] = useState<ComboBoxItems[] | []>([])
const navigate = useNavigate()
const { mutate, isPending } = useMutation({
mutationFn: registerMutation,
mutationKey: ["register"]
})
const { data: provinces } = useQuery({
queryFn: getProvincesList,
queryKey: ["province"]
})
const { data: cities } = useQuery({
queryFn: getCitiesList,
queryKey: ["cities"]
})
const { handleSubmit, formState: { errors }, control, watch } = useForm<RegisterInterface>({
resolver: yupResolver(RegisterSchema) resolver: yupResolver(RegisterSchema)
}) })
const provinceName: any = watch("province_name");
const handleRegister: SubmitHandler<RegisterInterface> = async (data) => { const handleRegister: SubmitHandler<RegisterInterface> = async (data) => {
console.log("data=>", data) mutate({
...data,
province_name: data?.province_name?.name,
city_name: data?.province_name?.name
},
{
onSuccess: () => {
toast.success("با موفقیت ثبت نام شدید")
navigate("/auth/login")
},
onError: (error: any) => {
console.log("error =>", error)
}
})
} }
useEffect(() => {
const tempArray: ComboBoxItems[] = [...provinceData]
provinces?.data?.data?.map((province: ProvinceType) => tempArray.push({
id: province?.ProvinceID,
name: province?.ProvinceName
}))
setProvinceData(tempArray)
}, [provinces])
useEffect(() => {
const tempCities: any = []
const newCities = cities?.data?.data?.filter((city: CitiesType) => city?.ProvinceName.toLocaleLowerCase().includes(provinceName?.name.toLowerCase()))
newCities?.map((city: CitiesType) => tempCities?.push({
id: city?.CityID,
name: city?.CityName
}))
setCitiesData(tempCities)
}, [provinceName]);
return ( return (
<form onSubmit={handleSubmit(handleRegister)} <form onSubmit={handleSubmit(handleRegister)}
className="w-full sm:w-fit sm:min-w-[500px] max-h-screen overflow-y-auto xl:w-1/2 xl:p-0 xl:py-5 p-10 flex flex-col bg-auth-form items-center gap-y-[42px] rounded-3x rounded-3xl"> className="w-full sm:w-fit sm:min-w-[500px] max-h-screen overflow-y-auto xl:w-1/2 xl:p-0 xl:py-5 p-10 flex flex-col bg-auth-form items-center gap-y-[42px] rounded-3x rounded-3xl">
@@ -31,43 +86,47 @@ export const RegisterForm = () => {
</div> </div>
<div className="grid grid-cols-1 gap-y-5 mb-[42px] mt-9"> <div className="grid grid-cols-1 gap-y-5 mb-[42px] mt-9">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-4 gap-y-5"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-x-4 gap-y-5">
<Controller control={control} name="name" render={({ field }) => <div> <Controller control={control} name="first_name" render={({ field }) => <div>
<Input type="text" placeholder="نام" field={field} /> <Input type="text" placeholder="نام" field={field} />
<ErrorComponent show={errors.name} message={errors.name?.message} /> <ErrorComponent show={errors.first_name} message={errors.first_name?.message} />
</div>} /> </div>} />
<Controller control={control} name="family" render={({ field }) => <div> <Controller control={control} name="last_name" render={({ field }) => <div>
<Input type="text" placeholder="نام خانوادگی" field={field} /> <Input type="text" placeholder="نام خانوادگی" field={field} />
<ErrorComponent show={errors.family} message={errors.family?.message} /> <ErrorComponent show={errors.last_name} message={errors.last_name?.message} />
</div>} /> </div>} />
</div> </div>
<div className="flex flex-col lg:flex-row gap-y-5 gap-x-4"> <Controller control={control} name="national_code" render={({ field }) => (<div>
<Controller control={control} name="capCity" render={({ field }) => <div> <Input type="text" placeholder="کد ملی" field={field} />
<ComboBox field={field} /> <ErrorComponent show={errors.national_code} message={errors.national_code?.message} />
<ErrorComponent show={errors.capCity} message={errors.capCity?.message} />
</div>} />
<Controller control={control} name="city" render={({ field }) => <div>
<ComboBox field={field} />
<ErrorComponent show={errors.city} message={errors.city?.message} />
</div>} />
</div>
<Controller control={control} name="marketName" render={({ field }) => (<div>
<Input type="text" placeholder="نام فروشگاه" field={field} />
<ErrorComponent show={errors.marketName} message={errors.marketName?.message} />
</div>)} /> </div>)} />
<Controller control={control} name="phoneNumber" render={({ field }) => <div> <div className="flex flex-col lg:flex-row gap-y-5 gap-x-4">
<Controller control={control} name="province_name" render={({ field }) => <div>
<ComboBox field={field} data={provinceData} />
<ErrorComponent show={errors.province_name} message={errors.province_name?.message} />
</div>} />
<Controller control={control} name="city_name" render={({ field }) => <div>
<ComboBox field={field} data={citiesData} />
<ErrorComponent show={errors.city_name} message={errors.city_name?.message} />
</div>} />
</div>
<Controller control={control} name="shopName" render={({ field }) => (<div>
<Input type="text" placeholder="نام فروشگاه" field={field} />
<ErrorComponent show={errors.shopName} message={errors.shopName?.message} />
</div>)} />
<Controller control={control} name="mobile_number" render={({ field }) => <div>
<Input type="number" placeholder="شماره موبایل" field={field} /> <Input type="number" placeholder="شماره موبایل" field={field} />
<ErrorComponent show={errors.phoneNumber} message={errors.phoneNumber?.message} /> <ErrorComponent show={errors.mobile_number} message={errors.mobile_number?.message} />
</div>} /> </div>} />
<Controller control={control} name="password" render={({ field }) => <div> <Controller control={control} name="password" render={({ field }) => <div>
<Input type="password" placeholder="رمز عبور" field={field} /> <Input type="password" placeholder="رمز عبور" field={field} />
<ErrorComponent show={errors.password} message={errors.password?.message} /> <ErrorComponent show={errors.password} message={errors.password?.message} />
</div>} /> </div>} />
<Controller control={control} name="repeatPassword" render={({ field }) => <div> <Controller control={control} name="password_confirmation" render={({ field }) => <div>
<Input type="password" placeholder="تکرار رمز عبور" field={field} /> <Input type="password" placeholder="تکرار رمز عبور" field={field} />
<ErrorComponent show={errors.repeatPassword} message={errors.repeatPassword?.message} /> <ErrorComponent show={errors.password_confirmation} message={errors.password_confirmation?.message} />
</div>} /> </div>} />
</div> </div>
<Button type="submit">ثبت نام</Button> <ButtonComponent title="ثبت نام" pending={isPending} type="submit" />
</div> </div>
</form> </form>
) )
@@ -1,19 +1,79 @@
import { Button } from "@headlessui/react";
import { Controller, SubmitHandler, useForm } from "react-hook-form"; import { Controller, SubmitHandler, useForm } from "react-hook-form";
import OTPInput from "react-otp-input" import OTPInput from "react-otp-input"
import { Link } from "react-router-dom" import { Link, useNavigate, useSearchParams } from "react-router-dom"
import { ResetPasswordCodeInterface } from "../../types"; import { ResetPasswordCodeInterface } from "../../types";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { ResetPasswordCodeSchema } from "../../schema"; import { ResetPasswordCodeSchema } from "../../schema";
import { ErrorComponent } from "../common/error"; import { ErrorComponent } from "../common/error";
import { useMutation } from "@tanstack/react-query";
import { checkOTP, sendOTPMutation } from "../../services/api/auth";
import toast from "react-hot-toast";
import { useEffect, useState } from "react";
import { ButtonComponent } from "../common/button";
export const ResetpasswordCodeForm = () => { export const ResetpasswordCodeForm = () => {
const navigate = useNavigate()
const [seconds, setSeconds] = useState(30);
const [isTimeUp, setIsTimeUp] = useState(false);
const [searchParams] = useSearchParams()
const { mutate, isPending } = useMutation({
mutationFn: checkOTP,
mutationKey: ["check-otp"]
})
const { mutate: sendOTPMutate, isPending: sendOTPPending } = useMutation({
mutationFn: sendOTPMutation,
mutationKey: ["send-otp"]
})
const { handleSubmit, formState: { errors }, control } = useForm<ResetPasswordCodeInterface>({ const { handleSubmit, formState: { errors }, control } = useForm<ResetPasswordCodeInterface>({
resolver: yupResolver(ResetPasswordCodeSchema) resolver: yupResolver(ResetPasswordCodeSchema)
}) })
const submitCode: SubmitHandler<ResetPasswordCodeInterface> = async (data) => { const submitCode: SubmitHandler<ResetPasswordCodeInterface> = async (data) => {
console.log("data=>", data) await mutate({
...data,
mobile_number: searchParams.get("phone_number"),
},
{
onSuccess: () => {
toast.success(`کد وارد شده مطابقت دارد`)
navigate(`/auth/reset-password?mobile_number=${searchParams.get("phone_number")}&otp=${data?.otp}`)
},
onError: (err: any) => {
console.log("err=>", err)
navigate(`/auth/reset-password?mobile_number=${searchParams.get("phone_number")}&otp=${data?.otp}`)
}
})
} }
const handleSendOTPAgain = async () => {
await sendOTPMutate({
mobile_number: searchParams.get("phone_number")
},
{
onSuccess: () => {
toast.success(`کد ورود برای شماره ${searchParams.get("phone_number")} ارسال شد`)
setSeconds(30)
setIsTimeUp(false)
},
onError: (err: any) => {
console.log("err=>", err)
}
})
}
useEffect(() => {
if (seconds > 0) {
const timer = setInterval(() => {
setSeconds(prevSeconds => prevSeconds - 1);
}, 1000);
return () => clearInterval(timer);
} else {
setIsTimeUp(true);
}
}, [seconds]);
return ( return (
<form onSubmit={handleSubmit(submitCode)} <form onSubmit={handleSubmit(submitCode)}
className="w-fit xl:w-1/2 xl:p-0 p-10 flex flex-col bg-auth-form justify-center items-center gap-y-[42px] rounded-3xl"> className="w-fit xl:w-1/2 xl:p-0 p-10 flex flex-col bg-auth-form justify-center items-center gap-y-[42px] rounded-3xl">
@@ -23,7 +83,7 @@ export const ResetpasswordCodeForm = () => {
<span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">فراموشی رمز عبور</span> <span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">فراموشی رمز عبور</span>
<div className="flex flex-row gap-x-0.5 items-center"> <div className="flex flex-row gap-x-0.5 items-center">
<div className="flex flex-col sm:flex-row gap-0.5 items-center"> <div className="flex flex-col sm:flex-row gap-0.5 items-center">
<p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره 09376225448 را وارد کنید.</p> <p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره {searchParams.get("phone_number")} را وارد کنید.</p>
<Link to="/auth/forgot-password"> <Link to="/auth/forgot-password">
<p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p> <p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p>
</Link> </Link>
@@ -31,7 +91,7 @@ export const ResetpasswordCodeForm = () => {
</div> </div>
</div> </div>
<div className="mt-9 mb-[42px] flex w-full justify-center items-center" id="otp"> <div className="mt-9 mb-[42px] flex w-full justify-center items-center" id="otp">
<Controller control={control} name="code" render={({ field }) => ( <Controller control={control} name="otp" render={({ field }) => (
<div> <div>
<OTPInput <OTPInput
containerStyle="otp-container" containerStyle="otp-container"
@@ -40,15 +100,15 @@ export const ResetpasswordCodeForm = () => {
renderInput={(props) => <input {...props} />} renderInput={(props) => <input {...props} />}
inputStyle="otp-inputs" inputStyle="otp-inputs"
/> />
<ErrorComponent show={errors.code} message={errors.code?.message} /> <ErrorComponent show={errors.otp} message={errors.otp?.message} />
</div> </div>
)} /> )} />
</div> </div>
<div className="flex flex-col gap-y-[27px] items-center"> <div className="flex flex-col gap-y-[27px] items-center">
<p className="font-normal text-secondary-text-color text-base">30 ثانیه دیگر تا دریافت کد</p> <p className="font-normal text-secondary-text-color text-base">{seconds} ثانیه دیگر تا دریافت کد</p>
<Button type="submit">تایید</Button> <ButtonComponent title="تایید" pending={isPending} type="submit" />
<p className="font-medium text-base text-primary-color">ارسال مجدد</p> <ButtonComponent title="ارسال مجدد" type="button" theme="glass" className="w-fit h-fit p-0" disable={!isTimeUp} pending={sendOTPPending} onClick={handleSendOTPAgain} />
</div> </div>
</div> </div>
</form> </form>
+27 -7
View File
@@ -1,14 +1,23 @@
import { Link } from "react-router-dom" import { Link, useNavigate, useSearchParams } from "react-router-dom"
import { Input } from "../common/input" import { Input } from "../common/input"
import { Controller, SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { ResetPasswordInterface } from "../../types" import { ResetPasswordInterface } from "../../types"
import { yupResolver } from "@hookform/resolvers/yup" import { yupResolver } from "@hookform/resolvers/yup"
import { resetPasswordSchema } from "../../schema" import { resetPasswordSchema } from "../../schema"
import { Button } from "@headlessui/react"
import { ErrorComponent } from "../common/error" import { ErrorComponent } from "../common/error"
import { useMutation } from "@tanstack/react-query"
import { resetPasswordMutation } from "../../services/api/auth"
import { ButtonComponent } from "../common/button"
import toast from "react-hot-toast"
export const ResetPasswordForm = () => { export const ResetPasswordForm = () => {
const navigate = useNavigate()
const { mutate, isPending } = useMutation({
mutationFn: resetPasswordMutation,
mutationKey: ["reset-password"]
})
const [searchParams] = useSearchParams()
const { const {
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
@@ -17,7 +26,18 @@ export const ResetPasswordForm = () => {
resolver: yupResolver(resetPasswordSchema) resolver: yupResolver(resetPasswordSchema)
}) })
const handleResetPassword: SubmitHandler<ResetPasswordInterface> = async (data) => { const handleResetPassword: SubmitHandler<ResetPasswordInterface> = async (data) => {
console.log("data=>", data) await mutate({
...data,
mobile_number: searchParams.get("mobile_number"),
otp: searchParams.get("otp")
},
{
onSuccess: () => {
toast.success("تغییر رمز عبور با موفقیت انجام شد")
navigate("/auth/login")
},
onError: (err: any) => console.log("err=>", err)
})
} }
return ( return (
<form onSubmit={handleSubmit(handleResetPassword)} <form onSubmit={handleSubmit(handleResetPassword)}
@@ -27,7 +47,7 @@ export const ResetPasswordForm = () => {
<div className="flex flex-col gap-y-5"> <div className="flex flex-col gap-y-5">
<span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">بازیابی رمز عبور</span> <span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">بازیابی رمز عبور</span>
<div className="flex flex-col md:flex-row gap-0.5 items-center"> <div className="flex flex-col md:flex-row gap-0.5 items-center">
<p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره 09376225448 را وارد کنید.</p> <p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره {searchParams.get("mobile_number")} را وارد کنید.</p>
<Link to="/auth/forgot-password"> <Link to="/auth/forgot-password">
<p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p> <p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p>
</Link> </Link>
@@ -38,12 +58,12 @@ export const ResetPasswordForm = () => {
<Input type="password" placeholder="رمز عبور" field={field} /> <Input type="password" placeholder="رمز عبور" field={field} />
<ErrorComponent show={errors.password} message={errors.password?.message} /> <ErrorComponent show={errors.password} message={errors.password?.message} />
</div>)} /> </div>)} />
<Controller control={control} name="repeatPassword" render={({ field }) => (<div> <Controller control={control} name="password_confirmation" render={({ field }) => (<div>
<Input type="password" placeholder="تکرار رمز عبور" field={field} /> <Input type="password" placeholder="تکرار رمز عبور" field={field} />
<ErrorComponent show={errors.repeatPassword} message={errors.repeatPassword?.message} /> <ErrorComponent show={errors.password_confirmation} message={errors.password_confirmation?.message} />
</div>)} /> </div>)} />
</div> </div>
<Button type="submit">ذخیره</Button> <ButtonComponent title="ذخیره" pending={isPending} type="submit" />
</div> </div>
</form> </form>
) )
+65 -12
View File
@@ -1,19 +1,72 @@
import OTPInput from "react-otp-input" import OTPInput from "react-otp-input"
import { Link } from "react-router-dom" import { Link, useSearchParams } from "react-router-dom"
import { ErrorComponent } from "../common/error" import { ErrorComponent } from "../common/error"
import { Controller, SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { Button } from "@headlessui/react" import { LoginWithOTP } from "../../types"
import { LoginCodeInterface } from "../../types"
import { yupResolver } from "@hookform/resolvers/yup" import { yupResolver } from "@hookform/resolvers/yup"
import { LoginCodeSchema } from "../../schema" import { LoginCodeSchema } from "../../schema"
import { useEffect, useState } from "react"
import { ButtonComponent } from "../common/button"
import { useMutation } from "@tanstack/react-query"
import { loginWithOTP, sendOTPMutation } from "../../services/api/auth"
import toast from "react-hot-toast"
export const SendCodeForm = () => { export const SendCodeForm = () => {
const { handleSubmit, formState: { errors }, control } = useForm<LoginCodeInterface>({ const { mutate, isPending } = useMutation({
mutationFn: sendOTPMutation,
mutationKey: ["send-otp"]
})
const { mutate: loginOtpMutate, isPending: loginOtpPending } = useMutation({
mutationFn: loginWithOTP,
mutationKey: ["login-otp"]
})
const [seconds, setSeconds] = useState(30);
const [isTimeUp, setIsTimeUp] = useState(false);
const [searchParams] = useSearchParams();
const { handleSubmit, formState: { errors }, control } = useForm<LoginWithOTP>({
resolver: yupResolver(LoginCodeSchema) resolver: yupResolver(LoginCodeSchema)
}) })
const submitCode: SubmitHandler<LoginCodeInterface> = async (data) => { const handleSendOTPAgain = async () => {
console.log("data=>", data) await mutate({
mobile_number: searchParams.get("phone_number")
},
{
onSuccess: () => {
toast.success(`کد ورود برای شماره ${searchParams.get("phone_number")} ارسال شد`)
setSeconds(30)
setIsTimeUp(false)
},
onError: (err: any) => {
console.log("err=>", err)
}
})
} }
const submitCode: SubmitHandler<LoginWithOTP> = async (data) => {
loginOtpMutate({
...data,
mobile_number: searchParams.get("phone_number"),
},
{
onSuccess: () => {
toast.success(`کد ورود برای شماره ${searchParams.get("phone_number")} ارسال شد`)
setSeconds(30)
setIsTimeUp(false)
},
onError: (err: any) => {
console.log("err=>", err)
}
})
}
useEffect(() => {
if (seconds > 0) {
const timer = setInterval(() => {
setSeconds(prevSeconds => prevSeconds - 1);
}, 1000);
return () => clearInterval(timer);
} else {
setIsTimeUp(true);
}
}, [seconds]);
return ( return (
<form onSubmit={handleSubmit(submitCode)} <form onSubmit={handleSubmit(submitCode)}
className="w-fit xl:w-1/2 xl:p-0 p-10 flex flex-col bg-auth-form justify-center items-center gap-y-[42px] rounded-3xl"> className="w-fit xl:w-1/2 xl:p-0 p-10 flex flex-col bg-auth-form justify-center items-center gap-y-[42px] rounded-3xl">
@@ -23,7 +76,7 @@ export const SendCodeForm = () => {
<span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">ورود با رمز یکبار مصرف</span> <span className="font-medium text-lg md:text-2xl xl:text-3xl text-primary-text-color">ورود با رمز یکبار مصرف</span>
<div className="flex flex-row gap-x-0.5 items-center"> <div className="flex flex-row gap-x-0.5 items-center">
<div className="flex flex-col sm:flex-row gap-0.5 items-center"> <div className="flex flex-col sm:flex-row gap-0.5 items-center">
<p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره 09376225448 را وارد کنید.</p> <p className="text-secondary-text-color font-normal text-xs md:text-sm">کد تایید ارسال شده به شماره {searchParams.get("phone_number")} را وارد کنید.</p>
<Link to="/auth/login-with-code"> <Link to="/auth/login-with-code">
<p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p> <p className="text-primary-color font-bold text-xs md:text-sm cursor-pointer">ویرایش</p>
</Link> </Link>
@@ -31,7 +84,7 @@ export const SendCodeForm = () => {
</div> </div>
</div> </div>
<div className="mt-9 mb-[42px] flex w-full justify-center items-center" id="otp"> <div className="mt-9 mb-[42px] flex w-full justify-center items-center" id="otp">
<Controller control={control} name="code" render={({ field }) => ( <Controller control={control} name="otp" render={({ field }) => (
<div> <div>
<OTPInput <OTPInput
containerStyle="otp-container" containerStyle="otp-container"
@@ -40,15 +93,15 @@ export const SendCodeForm = () => {
renderInput={(props) => <input {...props} />} renderInput={(props) => <input {...props} />}
inputStyle="otp-inputs" inputStyle="otp-inputs"
/> />
<ErrorComponent show={errors.code} message={errors.code?.message} /> <ErrorComponent show={errors.otp} message={errors.otp?.message} />
</div> </div>
)} /> )} />
</div> </div>
<div className="flex flex-col gap-y-[27px] items-center"> <div className="flex flex-col gap-y-[27px] items-center">
<p className="font-normal text-secondary-text-color text-base">30 ثانیه دیگر تا دریافت کد</p> {!isTimeUp && <p className="font-normal text-secondary-text-color text-base">{seconds} ثانیه دیگر تا دریافت کد</p>}
<Button type="submit">تایید</Button> <ButtonComponent title="تایید" pending={loginOtpPending} type="submit" />
<p className="font-medium text-base text-primary-color">ارسال مجدد</p> <ButtonComponent title="ارسال مجدد" type="button" theme="glass" className="w-fit h-fit p-0" disable={!isTimeUp} pending={isPending} onClick={handleSendOTPAgain} />
</div> </div>
</div> </div>
</form> </form>
+7 -1
View File
@@ -13,10 +13,16 @@
} }
@layer base { @layer base {
* { * {
font-family: 'vazirmatn', 'vaziren'; font-family: 'vazirmatn', 'vaziren';
}
body {
direction: rtl; direction: rtl;
scrollbar-width: none;
} }
input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-outer-spin-button,
@@ -38,7 +44,7 @@
} }
#otp { #otp {
direction: ltr; direction: ltr !important;
} }
} }
+21 -12
View File
@@ -13,13 +13,13 @@ export const loginSchema = yup.object({
export const resetPasswordSchema = yup.object({ export const resetPasswordSchema = yup.object({
password: yup.string().min(8, "رمز عبور نمیتواند کمتر از 8 کاراکتر باشد").required("رمز عبور الزامی میباشد"), password: yup.string().min(8, "رمز عبور نمیتواند کمتر از 8 کاراکتر باشد").required("رمز عبور الزامی میباشد"),
repeatPassword: yup.string() password_confirmation: yup.string()
.oneOf([yup.ref('password')], "رمز عبور و تکرار آن باید یکسان باشند") .oneOf([yup.ref('password')], "رمز عبور و تکرار آن باید یکسان باشند")
.required("تکرار رمز عبور الزامی میباشد"), .required("تکرار رمز عبور الزامی میباشد"),
}) })
export const forgotPasswordSchema = yup.object({ export const forgotPasswordSchema = yup.object({
phoneNumber: yup.string() mobile_number: yup.string()
.required("وارد کردن شماره تماس الزامی میباشد") .required("وارد کردن شماره تماس الزامی میباشد")
.min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد") .min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد")
.max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد") .max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد")
@@ -27,7 +27,7 @@ export const forgotPasswordSchema = yup.object({
}) })
export const LoginWithCodeSchema = yup.object({ export const LoginWithCodeSchema = yup.object({
phoneNumber: yup.string() mobile_number: yup.string()
.required("وارد کردن شماره تماس الزامی میباشد") .required("وارد کردن شماره تماس الزامی میباشد")
.min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد") .min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد")
.max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد") .max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد")
@@ -35,7 +35,11 @@ export const LoginWithCodeSchema = yup.object({
}) })
export const ResetPasswordCodeSchema = yup.object({ export const ResetPasswordCodeSchema = yup.object({
code: yup.string() mobile_number: yup.string()
.min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد")
.max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد")
.matches(/^[0-9]{11}$/, "شماره تماس نامعتبر"),
otp: yup.string()
.required("وارد کردن کد الزامی میباشد") .required("وارد کردن کد الزامی میباشد")
.min(5, "کد نمیتوند کمتر از 5 رقم باشد") .min(5, "کد نمیتوند کمتر از 5 رقم باشد")
.max(5, "کد نمیتواند بیشتر از 5 رقم باشد") .max(5, "کد نمیتواند بیشتر از 5 رقم باشد")
@@ -43,7 +47,11 @@ export const ResetPasswordCodeSchema = yup.object({
}) })
export const LoginCodeSchema = yup.object({ export const LoginCodeSchema = yup.object({
code: yup.string() mobile_number: yup.string()
.min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد")
.max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد")
.matches(/^[0-9]{11}$/, "شماره تماس نامعتبر"),
otp: yup.string()
.required("وارد کردن کد الزامی میباشد") .required("وارد کردن کد الزامی میباشد")
.min(5, "کد نمیتوند کمتر از 5 رقم باشد") .min(5, "کد نمیتوند کمتر از 5 رقم باشد")
.max(5, " کد نمیتواند بیشتر از 5 رقم باشد") .max(5, " کد نمیتواند بیشتر از 5 رقم باشد")
@@ -51,18 +59,19 @@ export const LoginCodeSchema = yup.object({
}) })
export const RegisterSchema = yup.object({ export const RegisterSchema = yup.object({
name: yup.string().required("وارد کردن نام الزامی میباشد").min(3, "نام نمیتواند کمتر از 3 کاراکتر باشد").max(20, "نام نمیتواند بیشتر از 20 کاراکتر باشد"), first_name: yup.string().required("وارد کردن نام الزامی میباشد").min(3, "نام نمیتواند کمتر از 3 کاراکتر باشد").max(20, "نام نمیتواند بیشتر از 20 کاراکتر باشد"),
family: yup.string().required("وارد کردن نام خانوادگی الزامی میباشد").min(3, "نام خانوادگی نمیتواند کمتر از 3 کاراکتر باشد").max(25, "نام خانوادگی نمیتواند بیشتر از 25 کاراکتر باشد"), last_name: yup.string().required("وارد کردن نام خانوادگی الزامی میباشد").min(3, "نام خانوادگی نمیتواند کمتر از 3 کاراکتر باشد").max(25, "نام خانوادگی نمیتواند بیشتر از 25 کاراکتر باشد"),
city: yup.object().required("وارد کردن شهر الزامی مباشد"), national_code: yup.string().required("وارد کردن کد ملی الزامی میباشد").min(10, "کد ملی نمیتواند کمتر از 10 کاراکتر باشد").max(10, "کد ملی نمیتونه بیشتر از 10 کاراکتر باشه").matches(/^[0-9]{10}$/, "کد نامعتبر"),
capCity: yup.object().required("وارد کردن استان الزامی مباشد"), city_name: yup.object().required("وارد کردن شهر الزامی مباشد"),
marketName: yup.string().required("وارد کردن نام فروشگاه الزامی میباشد").min(2, "نام فروشگاه نمیتواند کمتر از 3 کاراکتر باشد").max(30, "نام فروشگاه نمیتواند بیشتر از 30 کاراکتر باشد"), province_name: yup.object().required("وارد کردن استان الزامی مباشد"),
phoneNumber: yup.string() shopName: yup.string().required("وارد کردن نام فروشگاه الزامی میباشد").min(2, "نام فروشگاه نمیتواند کمتر از 3 کاراکتر باشد").max(30, "نام فروشگاه نمیتواند بیشتر از 30 کاراکتر باشد"),
mobile_number: yup.string()
.required("وارد کردن شماره تماس الزامی میباشد") .required("وارد کردن شماره تماس الزامی میباشد")
.min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد") .min(11, "شماره تماس نمیتوند کمتر از 11 رقم باشد")
.max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد") .max(11, "شماره تماس نمیتواند بیشتر از 11 رقم باشد")
.matches(/^[0-9]{11}$/, "شماره تماس نامعتبر"), .matches(/^[0-9]{11}$/, "شماره تماس نامعتبر"),
password: yup.string().min(8, "رمز عبور نمیتواند کمتر از 8 کاراکتر باشد").required("رمز عبور الزامی میباشد"), password: yup.string().min(8, "رمز عبور نمیتواند کمتر از 8 کاراکتر باشد").required("رمز عبور الزامی میباشد"),
repeatPassword: yup.string() password_confirmation: yup.string()
.oneOf([yup.ref('password')], "رمز عبور و تکرار آن باید یکسان باشند") .oneOf([yup.ref('password')], "رمز عبور و تکرار آن باید یکسان باشند")
.required("تکرار رمز عبور الزامی میباشد"), .required("تکرار رمز عبور الزامی میباشد"),
}) })
+7 -1
View File
@@ -1,3 +1,9 @@
import { LoginWithCodeInterface, LoginWithOTP, RegisterInterface, ResetPasswordInterface } from "../../types";
import axiosInstance from "../axios"; import axiosInstance from "../axios";
export const loginMutation = (payload: any) => axiosInstance.post<any>("/auth/gps/login/pass", payload) export const loginMutation = (payload: any) => axiosInstance.post<any>("/auth/gps/login/pass", payload)
export const registerMutation = (payload: RegisterInterface) => axiosInstance.post<any>("/auth/gps/register", payload)
export const sendOTPMutation = (payload: LoginWithCodeInterface) => axiosInstance.post<any>("/auth/gps/otp/send", payload)
export const loginWithOTP = (payload: LoginWithOTP) => axiosInstance.post<any>("/auth/gps/login/otp", payload)
export const checkOTP = (payload: LoginWithOTP) => axiosInstance.post<any>("/auth/gps/otp/check", payload)
export const resetPasswordMutation = (payload: ResetPasswordInterface) => axiosInstance.post<any>("/auth/gps/otp/reset-pass", payload)
+7
View File
@@ -0,0 +1,7 @@
import { CitiesResponse } from "../../types";
import axiosInstance from "../axios";
export const getCitiesList = () => axiosInstance.post<CitiesResponse>("/cross/getRouteManager", {
url: "/api/GetCity",
db: "verity"
})
+7
View File
@@ -0,0 +1,7 @@
import { ProvinceResponse } from "../../types";
import axiosInstance from "../axios";
export const getProvincesList = () => axiosInstance.post<ProvinceResponse, ProvinceResponse>("/cross/getRouteManager", {
url: "/api/GetProvince",
db: "verity"
})
+45 -13
View File
@@ -11,34 +11,39 @@ export interface LoginResponseInterface {
} }
export interface ResetPasswordInterface { export interface ResetPasswordInterface {
mobile_number?: string | any,
otp?: string | any,
password: string, password: string,
repeatPassword: string password_confirmation: string
} }
export interface ForgotPasswordInterface { export interface ForgotPasswordInterface {
phoneNumber: string mobile_number: string
} }
export interface LoginWithCodeInterface { export interface LoginWithCodeInterface {
phoneNumber: string mobile_number: string | any
} }
export interface ResetPasswordCodeInterface { export interface ResetPasswordCodeInterface {
code: string mobile_number?: string | any
otp: string
} }
export interface LoginCodeInterface { export interface LoginCodeInterface {
code: string mobile_number: any
otp: string
} }
export interface RegisterInterface { export interface RegisterInterface {
name: string, first_name: string
family: string, last_name: string
city: object, national_code: string
capCity: object, city_name: object | any
marketName: string, province_name: object | any
phoneNumber: string, shopName: string
password: string, mobile_number: string
repeatPassword: string, password: string
password_confirmation: string
} }
export interface SidebarInterface { export interface SidebarInterface {
@@ -280,4 +285,31 @@ export interface TableParams {
page: number, page: number,
rows: number, rows: number,
search: string search: string
}
export type ProvinceType = {
ProvinceID: number,
ProvinceName: string
}
export interface ProvinceResponse {
data: { data: ProvinceType[] | [], }
error: string | null
}
export interface LoginWithOTP {
mobile_number?: string | any
otp: string
}
export type CitiesType = {
CityID: number,
CityName: string,
ProvinceID: string,
ProvinceName: string
}
export interface CitiesResponse {
data: CitiesType[] | [],
error: string | null | any
} }