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-i18next": "^16.3.5",
"react-router-dom": "^7.9.6",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
@ -6563,6 +6564,19 @@
"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": {
"version": "1.0.10",
"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-i18next": "^16.3.5",
"react-router-dom": "^7.9.6",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",

View File

@ -18,7 +18,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
return (
<div className="w-full">
{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>
)}
@ -48,20 +48,7 @@ const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
{...props}
/>
{error && (
<p className="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>
<p className="justify-end mt-2 text-sm text-red-600 flex items-center gap-1">
{error}
</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 { createRoot } from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "./core/config/i18n.ts";
import "./index.css";
import { rootRoutes } from "./router/rootRoutes.ts";
@ -10,5 +11,17 @@ const router = createBrowserRouter(rootRoutes);
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
<ToastContainer
position="top-right"
autoClose={4000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={true}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
/>
</StrictMode>
);

View File

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

View File

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

View File

@ -3,7 +3,7 @@
import { useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
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 inputsRef = useRef<HTMLInputElement[]>([]);

View File

@ -14,7 +14,12 @@ import {
DialogTitle,
} from "@/core/components/base/dialog";
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 { toast } from "react-toastify";
import to from "await-to-js";
import { useState } from "react";
export function LoginPage() {
@ -23,6 +28,7 @@ export function LoginPage() {
const [phoneNumber, setPhoneNumber] = useState("");
const [otp, setOtp] = useState("");
const [error, setError] = useState("");
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
const validatePhoneNumber = (value: string) => {
const phoneRegex = /^[0-9]{10,11}$/;
@ -37,9 +43,24 @@ export function LoginPage() {
return true;
};
const handleSubmit = () => {
const handleSubmit = async () => {
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);
setOtpDialog(true);
setPhoneNumber("");
@ -51,6 +72,27 @@ export function LoginPage() {
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 (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
{/* Card */}
@ -109,6 +151,7 @@ export function LoginPage() {
<CustomButton
variant="primary"
className="w-full sm:w-auto"
disabled={submitLoading}
onClick={handleSubmit}
>
تایید
@ -122,7 +165,8 @@ export function LoginPage() {
otpValue={otp}
onChange={setOtp}
onClose={() => setIsDialogOpen(false)}
onSubmit={handleSubmit}
onSubmit={handleOtpSubmit}
submitLoading={submitLoading}
/>
</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 LoginPage from "../pages/login";
import RegisterPage from "../pages/register";
import { AUTH_ROUTE } from "./route.constant";
export const authRoutes: AppRoute[] = [
{
path: "/auth",
children: [
{ path: "login", element: <LoginPage /> },
{ path: "register", element: <RegisterPage /> },
{ path: AUTH_ROUTE.LOGIN, element: <LoginPage /> },
{ path: AUTH_ROUTE.REGISTER, element: <RegisterPage /> },
],
},
];