Add react-toastify for notifications and enhance OTP handling in LoginPage

This commit is contained in:
MehrdadAdabi 2025-11-22 19:22:04 +03:30
parent 38263c7a74
commit d9d97da7da
11 changed files with 101 additions and 21 deletions

14
package-lock.json generated
View File

@ -19,6 +19,7 @@
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-i18next": "^16.3.5", "react-i18next": "^16.3.5",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
@ -6563,6 +6564,19 @@
"react-dom": ">=18" "react-dom": ">=18"
} }
}, },
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",

View File

@ -21,6 +21,7 @@
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-i18next": "^16.3.5", "react-i18next": "^16.3.5",
"react-router-dom": "^7.9.6", "react-router-dom": "^7.9.6",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",

View File

@ -18,7 +18,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
<label className="mb-2 block text-sm font-medium text-foreground"> <label className="mb-2 block text-sm font-medium text-foreground text-right">
{label} {label}
</label> </label>
)} )}
@ -48,20 +48,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
{...props} {...props}
/> />
{error && ( {error && (
<p className="mt-2 text-sm text-red-600 flex items-center gap-1"> <p className="justify-end mt-2 text-sm text-red-600 flex items-center gap-1">
<svg
className="h-4 w-4"
fill="none"
strokeWidth="2"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
{error} {error}
</p> </p>
)} )}

View File

@ -0,0 +1,13 @@
export const API_ADDRESS = {
BASE_URL: "https://yarigaran-back.pelekan.org",
auth: {
otp: "/api/SignUpLoginBySMS",
verifyOtp: "/api/verifyloginbysms",
},
// LOGIN: "/auth/login",
// VERIFY_OTP: "/auth/verify-otp",
// REGISTER: "/auth/register",
// REFRESH_TOKEN: "/auth/refresh-token",
// USER_PROFILE: "/user/profile",
// UPDATE_PROFILE: "/user/update-profile",
};

View File

@ -1,6 +1,7 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "./core/config/i18n.ts"; import "./core/config/i18n.ts";
import "./index.css"; import "./index.css";
import { rootRoutes } from "./router/rootRoutes.ts"; import { rootRoutes } from "./router/rootRoutes.ts";
@ -10,5 +11,17 @@ const router = createBrowserRouter(rootRoutes);
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <RouterProvider router={router} />
<ToastContainer
position="top-right"
autoClose={4000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={true}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
</StrictMode> </StrictMode>
); );

View File

@ -16,6 +16,7 @@ export const OTPDialog: FC<OTPDialogProps> = ({
onClose, onClose,
onChange, onChange,
onSubmit, onSubmit,
submitLoading,
}) => { }) => {
return ( return (
<Dialog isOpen={isOpen} onClose={onClose}> <Dialog isOpen={isOpen} onClose={onClose}>
@ -35,6 +36,7 @@ export const OTPDialog: FC<OTPDialogProps> = ({
variant="primary" variant="primary"
className="w-full sm:w-auto" className="w-full sm:w-auto"
onClick={onSubmit} onClick={onSubmit}
disabled={submitLoading}
> >
تایید تایید
</CustomButton> </CustomButton>

View File

@ -4,4 +4,5 @@ export interface OTPDialogProps {
onClose: () => void; // برای بستن دیالوگ onClose: () => void; // برای بستن دیالوگ
onChange: (value: string) => void; // تغییر OTP onChange: (value: string) => void; // تغییر OTP
onSubmit: () => void; // تایید OTP onSubmit: () => void; // تایید OTP
submitLoading: boolean; // وضعیت بارگذاری تایید
} }

View File

@ -3,7 +3,7 @@
import { useRef, useState, type ChangeEvent, type KeyboardEvent } from "react"; import { useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import type { OTPReceiverProps } from "../../pages/login/login.type"; import type { OTPReceiverProps } from "../../pages/login/login.type";
export function OTPReceiver({ length = 4, onChange }: OTPReceiverProps) { export function OTPReceiver({ length = 5, onChange }: OTPReceiverProps) {
const [values, setValues] = useState(Array(length).fill("")); const [values, setValues] = useState(Array(length).fill(""));
const inputsRef = useRef<HTMLInputElement[]>([]); const inputsRef = useRef<HTMLInputElement[]>([]);

View File

@ -14,7 +14,12 @@ import {
DialogTitle, DialogTitle,
} from "@/core/components/base/dialog"; } from "@/core/components/base/dialog";
import { CustomInput } from "@/core/components/base/input"; import { CustomInput } from "@/core/components/base/input";
import { API_ADDRESS } from "@/core/service/api-address";
import API from "@/core/service/axios";
import { OTPDialog } from "@modules/auth/components/otp/opt-dialog"; import { OTPDialog } from "@modules/auth/components/otp/opt-dialog";
import { toast } from "react-toastify";
import to from "await-to-js";
import { useState } from "react"; import { useState } from "react";
export function LoginPage() { export function LoginPage() {
@ -23,6 +28,7 @@ export function LoginPage() {
const [phoneNumber, setPhoneNumber] = useState(""); const [phoneNumber, setPhoneNumber] = useState("");
const [otp, setOtp] = useState(""); const [otp, setOtp] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
const validatePhoneNumber = (value: string) => { const validatePhoneNumber = (value: string) => {
const phoneRegex = /^[0-9]{10,11}$/; const phoneRegex = /^[0-9]{10,11}$/;
@ -37,9 +43,24 @@ export function LoginPage() {
return true; return true;
}; };
const handleSubmit = () => { const handleSubmit = async () => {
if (validatePhoneNumber(phoneNumber)) { if (validatePhoneNumber(phoneNumber)) {
console.log("Phone number submitted:", phoneNumber); setSubmitLoading(true);
const [err, res] = await to(API.post(API_ADDRESS.auth.otp, phoneNumber));
setSubmitLoading(false);
if (res?.data.resultType !== 0) {
toast.error(res?.data.message);
}
if (res?.data.resultType === 0) {
toast.success("ورود با موفقیت انجام شد");
localStorage.setItem("token", res.data.data.token);
}
if (err) {
return;
}
setIsDialogOpen(false); setIsDialogOpen(false);
setOtpDialog(true); setOtpDialog(true);
setPhoneNumber(""); setPhoneNumber("");
@ -51,6 +72,27 @@ export function LoginPage() {
setError(""); setError("");
}; };
const handleOtpSubmit = async () => {
setSubmitLoading(true);
const [err, res] = await to(
API.post(API_ADDRESS.auth.verifyOtp, { phoneNumber, otp })
);
if (res?.data.resultType !== 0) {
toast.error(res?.data.message);
}
if (res?.data.resultType === 0) {
toast.success("ورود با موفقیت انجام شد");
localStorage.setItem("token", res.data.data.token);
}
setSubmitLoading(false);
if (err) {
return;
}
setOtpDialog(false);
};
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4"> <div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
{/* Card */} {/* Card */}
@ -109,6 +151,7 @@ export function LoginPage() {
<CustomButton <CustomButton
variant="primary" variant="primary"
className="w-full sm:w-auto" className="w-full sm:w-auto"
disabled={submitLoading}
onClick={handleSubmit} onClick={handleSubmit}
> >
تایید تایید
@ -122,7 +165,8 @@ export function LoginPage() {
otpValue={otp} otpValue={otp}
onChange={setOtp} onChange={setOtp}
onClose={() => setIsDialogOpen(false)} onClose={() => setIsDialogOpen(false)}
onSubmit={handleSubmit} onSubmit={handleOtpSubmit}
submitLoading={submitLoading}
/> />
</div> </div>
); );

View File

@ -0,0 +1,4 @@
export const AUTH_ROUTE = {
LOGIN: "login",
REGISTER: "register",
};

View File

@ -1,13 +1,14 @@
import type { AppRoute } from "@core/types/router.type"; import type { AppRoute } from "@core/types/router.type";
import LoginPage from "../pages/login"; import LoginPage from "../pages/login";
import RegisterPage from "../pages/register"; import RegisterPage from "../pages/register";
import { AUTH_ROUTE } from "./route.constant";
export const authRoutes: AppRoute[] = [ export const authRoutes: AppRoute[] = [
{ {
path: "/auth", path: "/auth",
children: [ children: [
{ path: "login", element: <LoginPage /> }, { path: AUTH_ROUTE.LOGIN, element: <LoginPage /> },
{ path: "register", element: <RegisterPage /> }, { path: AUTH_ROUTE.REGISTER, element: <RegisterPage /> },
], ],
}, },
]; ];