feat : login mobile or email complated

This commit is contained in:
alij
2024-06-16 01:25:58 +04:30
parent eb91223ed8
commit 6a53f9f912
4 changed files with 370 additions and 110 deletions

View File

@@ -0,0 +1,120 @@
import { motion } from "framer-motion";
import Input from "../../../components/input";
import Button from "../../../components/button";
import { useFormik } from "formik";
import * as Yup from "yup";
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../../redux/store";
import { setToken } from "../../../redux/slices/token";
import { useNavigate } from "react-router-dom";
import useAxios from "../../../hooks/useAxios";
const LoginEmailForm = () => {
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();
const axios = useAxios()
const mutationEmail = useMutation({
mutationFn: (values: { email: string; password: string }) => {
return axios.post("/login", values);
},
});
const formikEmail = useFormik({
initialValues: {
email: "",
password: "",
},
validationSchema: Yup.object({
email: Yup.string().email("ایمیل نامعتبر است"),
password: Yup.string().min(6, "رمز عبور حداقل باید 6 کاراکتر باشد"),
}),
onSubmit: (values) => {
toast.promise(mutationEmail.mutateAsync(values), {
loading: "در حال احراز هویت لطفا صبر نمایید...",
success: (res) => {
dispatch(setToken(res?.data?.result?.token));
navigate("/");
return `خوش آمدید ${res.data.alert}`;
},
error: (err: AxiosError) => {
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
typeof err.response.data === "object" &&
"message" in err.response.data
) {
errorMessage = (err.response.data as { message: string }).message;
}
return errorMessage;
},
});
},
});
return (
<>
<form
className="flex flex-col gap-10"
onSubmit={formikEmail.handleSubmit}
>
<div className="w-full h-full flex flex-col gap-4">
<motion.div
initial={{ x: "-100vw", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="flex flex-col gap-2"
>
<Input
id="email"
type="text"
label="ایمیل:"
placeholder="ایمیل"
extra={`${
formikEmail.errors.email && "!outline !outline-rose-600"
}`}
{...formikEmail.getFieldProps("email")}
/>
{formikEmail.errors.email && (
<p className="text-xs md:text-sm text-rose-600">
{formikEmail.errors.email}
</p>
)}
</motion.div>
<motion.div
initial={{ x: "100vw", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="flex flex-col gap-1"
>
<Input
id="password"
type="password"
label="رمز عبور:"
placeholder="رمز"
extra={`${
formikEmail.errors.password && "!outline !outline-rose-600"
}`}
{...formikEmail.getFieldProps("password")}
/>
{formikEmail.errors.password && (
<p className="text-xs md:text-sm text-rose-600">
{formikEmail.errors.password}
</p>
)}
</motion.div>
</div>
<div className="w-full h-full grid grid-cols-1">
<Button extra="py-2" type="submit">
ورود
</Button>
</div>
</form>
</>
);
};
export default LoginEmailForm;

View File

@@ -0,0 +1,236 @@
import { motion } from "framer-motion";
import Input from "../../../components/input";
import OTPInput from "react-otp-input";
import * as Yup from "yup";
import { useFormik } from "formik";
import Button from "../../../components/button";
import { Dispatch, SetStateAction, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import useAxios from "../../../hooks/useAxios";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../../redux/store";
import { AxiosError } from "axios";
import { setToken } from "../../../redux/slices/token";
import toast from "react-hot-toast";
const LoginMobileForm = ({
setisShowLoginNumberOrEmail,
}: {
setisShowLoginNumberOrEmail: Dispatch<SetStateAction<boolean>>;
}) => {
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();
const [stepMobile, setStepMobile] = useState<1 | 2>(1);
const axios = useAxios();
// Mutation for mobile login
const mutationMobile = useMutation({
mutationFn: (values: { mobile: string }) => {
return axios.post("/login-mobile", values);
},
});
// Mutation for code verification
const mutationCode = useMutation({
mutationFn: (values: { code: string }) => {
return axios.post("/verify", values);
},
});
// Formik for mobile step
const formikMobile = useFormik({
initialValues: {
mobile: "",
},
validationSchema: Yup.object({
mobile: Yup.string()
.matches(/^09\d{9}$/, "شماره باید با 09 شروع شود و باید 11 رقم باشد")
.required("لطفا شماره خود را برای ورود وارد کنید"),
}),
onSubmit: (values) => {
if (stepMobile === 1) {
toast.promise(mutationMobile.mutateAsync(values), {
loading: "در حال ارسال کد...",
success: (res) => {
dispatch(setToken(res?.data?.result?.token));
setisShowLoginNumberOrEmail(false)
setStepMobile(2);
return `${res.data.alert}`;
},
error: (err: AxiosError) => {
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
typeof err.response.data === "object" &&
"message" in err.response.data
) {
errorMessage = (err.response.data as { message: string }).message;
}
return errorMessage;
},
});
}
},
});
// Formik for code step
const formikMobileCode = useFormik({
initialValues: {
code: "",
},
validationSchema: Yup.object({
code: Yup.string()
.length(4, "لطفا 4 رقم کد را وارد کنید")
.required("لطفا تمام فیلد را پر کنید"),
}),
onSubmit: (values) => {
const mainValue = {
mobile: formikMobile.values.mobile,
code: values.code,
};
if (stepMobile === 2) {
toast.promise(mutationCode.mutateAsync(mainValue), {
loading: "در حال احراز هویت لطفا صبر نمایید...",
success: (res) => {
dispatch(setToken(res?.data?.result?.token));
navigate("/");
return `خوش آمدید ${res.data.alert}`;
},
error: (err: AxiosError) => {
let errorMessage = "یک خطای غیرمنتظره رخ داد";
if (
err.response?.data &&
typeof err.response.data === "object" &&
"message" in err.response.data
) {
errorMessage = (err.response.data as { message: string }).message;
}
return errorMessage;
},
});
}
},
});
// Handle OTP input change
const handleChangeMobileCode = (otp: string) => {
formikMobileCode.setFieldValue("code", otp);
};
return (
<>
<form
className="flex flex-col gap-10"
onSubmit={
stepMobile === 1
? formikMobile.handleSubmit
: formikMobileCode.handleSubmit
}
>
<div className="w-full h-full flex flex-col gap-2">
{stepMobile === 1 ? (
<motion.div
initial={{ x: "100vw", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="flex flex-col gap-2"
>
<Input
id="number"
type="text"
label="شماره:"
placeholder="شماره همراه"
extra={`${
formikMobile.errors.mobile && "!outline !outline-rose-600"
}`}
{...formikMobile.getFieldProps("mobile")}
/>
{formikMobile.errors.mobile && (
<p className="text-xs md:text-sm text-rose-600">
{formikMobile.errors.mobile}
</p>
)}
</motion.div>
) : (
stepMobile === 2 && (
<>
<motion.h1
initial={{ y: -20 }}
animate={{ y: 0 }}
className="text-base text-center leading-tight tracking-tight md:text-lg text-white mb-3"
>
کد ارسال شده رو وارد نمایید:
</motion.h1>
<div className="flex flex-col gap-2">
<OTPInput
value={formikMobileCode.values.code}
onChange={handleChangeMobileCode}
numInputs={4}
inputStyle={{
width: "3rem",
height: "3rem",
margin: "0 0.5rem",
fontSize: "2rem",
borderRadius: "4px",
border: "none",
outline: "none",
textAlign: "center",
direction: "ltr",
}}
shouldAutoFocus={true}
containerStyle={{
display: "flex",
flexDirection: "row-reverse",
justifyContent: "center",
}}
renderInput={(props) => (
<motion.input
{...props}
inputMode="numeric" // برای نمایش کیبورد عددی در دستگاه‌های موبایل
pattern="\d*" // برای محدود کردن ورودی به اعداد
onInput={(e) => {
const input = e.target as HTMLInputElement; // تایپ‌کست کردن به HTMLInputElement
input.value = input.value.replace(/\D/g, ""); // حذف کاراکترهای غیر عددی
}}
type="text" // تغییر از "number" به "text"
initial={{
y: 20,
opacity: 0,
border: "2px solid transparent",
}}
animate={{
y: 0,
opacity: 1,
}}
className={`bg-gray-800 focus:outline-none focus:ring-1 focus:ring-gray-600 shadow-md max-sm:!w-[2.5rem] max-sm:!h-[2.5rem] shadow-gray-900 ${
formikMobileCode.errors.code &&
"!ring-1 !ring-rose-600"
} ${props.className}`}
/>
)}
/>
{formikMobileCode.errors.code && (
<p className="text-xs md:text-sm text-rose-600 text-center">
{formikMobileCode.errors.code}
</p>
)}
</div>
</>
)
)}
</div>
<div className="w-full h-full grid grid-cols-1">
<Button extra="py-2" type="submit">
{stepMobile === 1 ? "دریافت کد" : "ورود"}
</Button>
</div>
</form>
</>
);
};
export default LoginMobileForm;

View File

@@ -1,123 +1,27 @@
// import Input from "../../components/input";
import Button from "../../components/button";
import { useState } from "react"; import { useState } from "react";
// import LoginNumberOrEmail from "./components/LoginNumberOrEmail";
import { motion } from "framer-motion"; import LoginNumberOrEmail from "./components/LoginNumberOrEmail";
// import Rabbit from "./components/Rabbit"; import LoginMobileForm from "./components/LoginMobileForm";
import OTPInput from "react-otp-input"; import LoginEmailForm from "./components/LoginEmailForm";
const Login = () => { const Login = () => {
// const [loginWithEmail, setLoginWithEmail] = useState<boolean>(false); const [loginWithEmail, setLoginWithEmail] = useState<boolean>(false);
// const [inputText, setInputText] = useState<boolean>(false); const [isShowLoginNumberOrEmail , setisShowLoginNumberOrEmail] = useState(true);
// const [inputPassword, setInputPassword] = useState<boolean>(false);
const [authCode, setAuthCode] = useState<boolean>(false);
const [errorCode, setErrorCode] = useState<boolean>(false);
const [otp, setOtp] = useState<string>("");
const handleChange = (otp: string) => {
setOtp(otp);
if (otp === "1111" && otp.length === 4)
setAuthCode(true), setErrorCode(false);
else if (otp.length === 4) setErrorCode(true), setAuthCode(false);
else setAuthCode(false), setErrorCode(false);
};
return ( return (
<> <>
<div className="w-4/5 rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0 glass"> <div className="w-4/5 rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0 glass">
{/* <Rabbit focusInputText={inputText} focusInputPassword={inputPassword} /> */}
<div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-2xl font-bold text-center leading-tight tracking-tight md:text-3xl text-white"> <h1 className="text-2xl font-bold text-center leading-tight tracking-tight md:text-3xl text-white">
ورود ورود
</h1> </h1>
<h1 className="text-base text-center leading-tight tracking-tight md:text-lg text-white"> {isShowLoginNumberOrEmail && (
کد ارسال شده رو وارد نمایید: <LoginNumberOrEmail
</h1> loginWithEmail={loginWithEmail}
{/* <LoginNumberOrEmail setLoginWithEmail={setLoginWithEmail}
loginWithEmail={loginWithEmail} />
setLoginWithEmail={setLoginWithEmail} )}
/> */} {!loginWithEmail ? <LoginMobileForm setisShowLoginNumberOrEmail={setisShowLoginNumberOrEmail} /> : <LoginEmailForm />}
<form className="flex flex-col gap-10">
<div className="w-full h-full flex flex-col gap-2">
{/* {!loginWithEmail && (
<motion.div
initial={{ x: "100vw", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
>
<Input
id="number"
type="text"
label="شماره:"
placeholder="sghl"
onFocus={() => setInputText(true)}
onBlur={() => setInputText(false)}
/>
</motion.div>
)}
{loginWithEmail && (
<motion.div
initial={{ x: "-100vw", opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
>
<Input
id="ss"
type="text"
label="ایمیل:"
placeholder="sghl"
onFocus={() => setInputText(true)}
onBlur={() => setInputText(false)}
/>
</motion.div>
)} */}
<OTPInput
value={otp}
onChange={handleChange}
numInputs={4}
inputStyle={{
width: "3rem",
height: "3rem",
margin: "0 0.5rem",
fontSize: "2rem",
borderRadius: "4px",
border: "none",
outline: "none",
textAlign: "center",
direction: "ltr",
}}
shouldAutoFocus={true}
containerStyle={{
display: "flex",
flexDirection: "row-reverse",
justifyContent: "center",
}}
renderInput={(props) => (
<motion.input
{...props}
type="number"
initial={{
y: 20,
opacity: 0,
border: "2px solid transparent",
}}
animate={{
y: 0,
opacity: 1,
border: `2px solid ${
authCode ? "green" : errorCode ? "red" : "transparent"
}`,
}}
className={`bg-gray-800 focus:outline-none focus:ring-1 focus:ring-gray-600 shadow-md max-sm:!w-[2.5rem] max-sm:!h-[2.5rem] shadow-gray-900 ${props.className}`}
/>
)}
/>
</div>
<div className="w-full h-full grid grid-cols-1">
<Button extra="py-2">تایید</Button>
</div>
</form>
</div> </div>
</div> </div>
</> </>

View File

@@ -2,7 +2,7 @@ import { Navigate, Outlet } from "react-router-dom";
import { isAuthenticated } from "../utils/auth"; import { isAuthenticated } from "../utils/auth";
const PrivateRoute = () => { const PrivateRoute = () => {
return !isAuthenticated() ? <Outlet /> : <Navigate to="/login" />; return isAuthenticated() ? <Outlet /> : <Navigate to="/login" />;
}; };
export default PrivateRoute; export default PrivateRoute;