This commit is contained in:
hamid zarghami
2024-12-24 11:43:25 +03:30
parent 67135204ab
commit 6d5ce71b2f
17 changed files with 609 additions and 51 deletions
+170 -6
View File
@@ -9,15 +9,19 @@
"version": "0.0.0",
"dependencies": {
"@tanstack/react-query": "^5.62.8",
"formik": "^2.4.6",
"i18next": "^24.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.2.0",
"react-loading": "^2.0.3",
"react-multi-date-picker": "^4.5.2",
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.0",
"react-toastify": "^11.0.2",
"tailwind-merge": "^2.5.5"
"tailwind-merge": "^2.5.5",
"yup": "^1.6.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -1441,6 +1445,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
"integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
"license": "MIT",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1452,14 +1466,12 @@
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2124,7 +2136,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -2152,6 +2163,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -2569,6 +2589,31 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/formik": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
"license": "Apache-2.0",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2721,6 +2766,15 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -3029,6 +3083,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3530,6 +3596,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3598,6 +3670,12 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
"license": "MIT"
},
"node_modules/react-i18next": {
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.2.0.tgz",
@@ -3624,8 +3702,7 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-loading": {
"version": "2.0.3",
@@ -3651,6 +3728,16 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-otp-input": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
"integrity": "sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.6 || ^17.0.0 || ^18.0.0",
"react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -4153,6 +4240,18 @@
"node": ">=0.8"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4166,6 +4265,12 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -4186,6 +4291,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
@@ -4205,6 +4316,18 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
@@ -4524,6 +4647,47 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/zustand": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz",
"integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}
+5 -1
View File
@@ -11,15 +11,19 @@
},
"dependencies": {
"@tanstack/react-query": "^5.62.8",
"formik": "^2.4.6",
"i18next": "^24.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-i18next": "^15.2.0",
"react-loading": "^2.0.3",
"react-multi-date-picker": "^4.5.2",
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.0",
"react-toastify": "^11.0.2",
"tailwind-merge": "^2.5.5"
"tailwind-merge": "^2.5.5",
"yup": "^1.6.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+6 -2
View File
@@ -39,18 +39,22 @@ const queryClient = new QueryClient({
const App: FC = () => {
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
useEffect(() => {
if (localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)) {
setIsLogin('isLogin')
} else {
setIsLogin('isNotLogin')
window.location.href = Pages.auth.login
if (window.location.href.split('auth').length === 1) {
window.location.href = Pages.auth.login
}
}
}, [])
console.log('us', isLogin);
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.97508 5.44165L2.91675 10.5L7.97508 15.5583" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.0834 10.5H3.05835" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 407 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 794 B

+15
View File
@@ -0,0 +1,15 @@
import { FC } from 'react'
type Props = {
errorText: string
}
const Error: FC<Props> = (props: Props) => {
return (
<div className='mt-1.5 font-normal text-red-500 text-[11px]'>
{props.errorText}
</div>
)
}
export default Error
+13 -2
View File
@@ -1,5 +1,6 @@
import { FC, InputHTMLAttributes } from 'react'
import { FC, InputHTMLAttributes, useState } from 'react'
import { clx } from '../helpers/utils';
import EyeIcon from '../assets/images/eye.svg'
type Variant = "floating_outlined" | "primary" | "search";
@@ -16,19 +17,29 @@ type Props = {
const Input: FC<Props> = (props: Props) => {
const [showPassword, setShowPassword] = useState<boolean>(false)
const inputClass = clx(
'w-full h-10 text-black block px-4 text-xs rounded-xl mt-1 border border-border',
props.readOnly && 'bg-gray-100',
props.className
);
return (
<div className='w-full'>
<label className='text-sm'>
{props.label}
</label>
<input {...props} className={inputClass} />
<div className='w-full relative'>
<input {...props} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
{
props.type === 'password' &&
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
}
</div>
</div>
)
}
+1
View File
@@ -2,5 +2,6 @@ export const Pages = {
auth: {
login: "/auth/login",
register: "/auth/register",
forgotPassword: "/auth/forgot",
},
};
+71
View File
@@ -0,0 +1,71 @@
import { useEffect, useState } from 'react';
export interface ICountDown {
m: number;
s: number;
}
/**
*
* @param minutes how many minutes.
* @param autoplay by default true. if you want play coundDown after an action, you can set it to false and manually play it.
* @param onEnd you can pass a callback function that will trigger at the END.
* @returns
*/
export function useCountDown(minutes: number, autoplay = true, onEnd = () => {}) {
const [countDown, setCountDown] = useState<ICountDown>({m: minutes, s: 0});
const [stop, setStop] = useState<boolean>(!autoplay);
useEffect(() => {
const handler = setTimeout(() => {
if (countDown.m !== 0 || countDown.s !== 0) {
setCountDown(calculateCountDown(countDown));
} else {
onEnd();
}
}, 1000);
return () => {
clearTimeout(handler);
};
}, [countDown]);
const reset = () => {
setCountDown({m: minutes, s: 0});
};
const pause = () => {
setStop(true);
};
const play = () => {
setStop(false);
setCountDown({...countDown});
};
const calculateCountDown = (st: ICountDown): ICountDown => {
if (stop) {
return countDown;
}
const timeLeft = { m: 0, s: 0 };
const remain = st.m * 60 + st.s - 1;
if (remain > 0) {
timeLeft.m = Math.floor((remain) / 60);
timeLeft.s = remain - timeLeft.m * 60;
return timeLeft;
} else {
if (remain === 0 && countDown.s === 1) {
return timeLeft;
} else {
return countDown;
}
}
};
return {
value: `${countDown.m > 9 ? countDown.m : '0' + countDown.m}:${countDown.s > 9 ? countDown.s : '0' + countDown.s}`,
reset,
pause,
play
};
}
+6
View File
@@ -47,3 +47,9 @@ textarea::placeholder {
.rmdp-container {
width: 100%;
}
.dltr {
direction: ltr;
}
.otp div {
width: 100%;
}
+15 -1
View File
@@ -24,6 +24,20 @@
"mobile_or_email": " شماره موبایل یا ایمیل",
"enter_mobile_or_email": " شماره موبایل یا ایمیل خود را وارد کنید",
"have_account": "آیا حساب کاربری ندارید؟",
"register": "ثبت نام"
"register": "ثبت نام",
"enter_otp": "کد تایید را وارد کنید",
"verfify_code_text_1": "کد تایید برای ",
"verfify_code_text_2": "ارسال شد.",
"change_number": "تغییر شماره",
"verify_code": "کد تایید ",
"counter_otp": "مانده تا دریافت مجدد کد",
"login_with_password": "ورود با رمز عبور",
"enter_password_step3": "رمز عبور را وارد کنید",
"enter_your_password": "رمز عبور خود را وارد کنید.",
"forgot_password": "فراموشی رمز عبور",
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
},
"errors": {
"required": "این فیلد اجباری می باشد"
}
}
+16 -39
View File
@@ -1,15 +1,15 @@
import { FC } from 'react'
import LogoImage from '../../assets/images/logo.svg'
import LogoSmallImage from '../../assets/images/logo-small.svg'
import { useTranslation } from 'react-i18next'
import Input from '../../components/Input'
import Button from '../../components/Button'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
import { useAuthStore } from '../store/AuthStore'
import LoginStep1 from './components/LoginStep1'
import LoginStep2 from './components/LoginStep2'
import LoginStep3 from './components/LoginStep3'
const Login: FC = () => {
const { t } = useTranslation('global')
const { stepLogin } = useAuthStore()
return (
<div className='w-full h-full flex justify-center py-[75px] items-center px-10'>
@@ -19,39 +19,16 @@ const Login: FC = () => {
<img src={LogoSmallImage} className='w-8' />
<div className='flex flex-1 flex-col h-full justify-center'>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.welcome')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_info_login')}
</p>
</div>
<div className='mt-16 rowTwoInput'>
<Input
label={t('auth.mobile_or_email')}
placeholder={t('auth.enter_mobile_or_email')}
type='tel'
className='text-right'
/>
</div>
<Button
label={t('auth.next')}
className='mt-8'
/>
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.have_account')}
</p>
<Link to={Pages.auth.register}>
<div>{t('auth.register')}</div>
</Link>
</div>
{
stepLogin === 1 ?
<LoginStep1 />
:
stepLogin === 2 ?
<LoginStep2 />
: stepLogin === 3 ?
<LoginStep3 />
: null
}
</div>
+82
View File
@@ -0,0 +1,82 @@
import { FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { LoginType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../../store/AuthStore'
import Error from '../../../components/Error'
import Button from '../../../components/Button'
import { Link } from 'react-router-dom'
import { Pages } from '../../../config/Pages'
const LoginStep1: FC = () => {
const { t } = useTranslation('global')
const { setPhone, phone, setStepLogin } = useAuthStore()
const formik = useFormik<LoginType>({
initialValues: {
phone_email: phone,
},
validationSchema: Yup.object({
phone_email: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
setPhone(values.phone_email)
setStepLogin(2)
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.welcome')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_info_login')}
</p>
</div>
<div className='mt-16'>
<Input
label={t('auth.mobile_or_email')}
placeholder={t('auth.enter_mobile_or_email')}
type='text'
className='text-right'
name='phone_email'
onChange={formik.handleChange}
value={formik.values.phone_email}
/>
{
formik.touched.phone_email && formik.errors.phone_email &&
<Error
errorText={formik.errors.phone_email}
/>
}
</div>
<Button
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
/>
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.have_account')}
</p>
<Link to={Pages.auth.register}>
<div>{t('auth.register')}</div>
</Link>
</div>
</div>
)
}
export default LoginStep1
+91
View File
@@ -0,0 +1,91 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LoginType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../../store/AuthStore'
import Button from '../../../components/Button'
import OTPInput from 'react-otp-input'
import { useCountDown } from '../../../hooks/useCountDown'
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
const LoginStep2: FC = () => {
const { t } = useTranslation('global')
const { setPhone, phone, setStepLogin } = useAuthStore()
const [otpValue, setOtpValue] = useState<string>()
const { value } = useCountDown(2, true)
const formik = useFormik<LoginType>({
initialValues: {
phone_email: '',
},
validationSchema: Yup.object({
phone_email: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
setPhone(values.phone_email)
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.enter_otp')}
</h2>
<p className='text-description flex gap-1 text-sm mt-2'>
<div>{t('auth.verfify_code_text_1')}</div>
<div>{phone}</div>
<div>
{t('auth.verfify_code_text_2')}
</div>
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
{t('auth.change_number')}
</div>
</p>
</div>
<div className='mt-16'>
<div className='text-sm'>
{t('auth.verify_code')}
</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
shouldAutoFocus
value={otpValue}
numInputs={5}
inputType='tel'
onChange={(otp: string) => setOtpValue(otp)}
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
/>
</div>
<div className='flex mt-4 justify-end'>
<div className='flex gap-1 text-description text-xs'>
<div>{value}</div>
<div>{t('auth.counter_otp')}</div>
</div>
</div>
</div>
<Button
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
/>
<div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.login_with_password')}
</p>
<img src={ArrowLeftIcon} className='w-5' />
</div>
</div>
)
}
export default LoginStep2
+84
View File
@@ -0,0 +1,84 @@
import { FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { LoginPasswordType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../../store/AuthStore'
import Error from '../../../components/Error'
import Button from '../../../components/Button'
import { Link } from 'react-router-dom'
import { Pages } from '../../../config/Pages'
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
const LoginStep3: FC = () => {
const { t } = useTranslation('global')
const { setStepLogin } = useAuthStore()
const formik = useFormik<LoginPasswordType>({
initialValues: {
password: '',
},
validationSchema: Yup.object({
phone_email: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
console.log(values);
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.enter_password_step3')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_your_password')}
</p>
</div>
<div className='mt-16'>
<Input
label={t('auth.password')}
type='password'
className='text-right'
name='password'
onChange={formik.handleChange}
value={formik.values.password}
/>
{
formik.touched.password && formik.errors.password &&
<Error
errorText={formik.errors.password}
/>
}
</div>
<Link to={Pages.auth.forgotPassword}>
<div className='mt-4 flex justify-end text-sm'>
{t('auth.forgot_password')}
</div>
</Link>
<Button
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
/>
<div onClick={() => setStepLogin(2)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.login_with_otp')}
</p>
<img src={ArrowLeftIcon} className='w-4' />
</div>
</div>
)
}
export default LoginStep3
+13
View File
@@ -0,0 +1,13 @@
export type LoginType = {
phone_email: string;
};
export type LoginPasswordType = {
password: string;
};
export type AuthStoreType = {
phone: string;
setPhone: (value: string) => void;
stepLogin: number;
setStepLogin: (value: number) => void;
};
+13
View File
@@ -0,0 +1,13 @@
import { create } from "zustand";
import { AuthStoreType } from "../auth/types/AuthTypes";
export const useAuthStore = create<AuthStoreType>((set) => ({
phone: "",
setPhone(value) {
set({ phone: value });
},
stepLogin: 1,
setStepLogin(value) {
set({ stepLogin: value });
},
}));