[FEAT]: add authentication page ui logic

This commit is contained in:
behnamrhp 2023-05-22 14:34:40 +03:00
parent 2b38085e42
commit 832220365e
9 changed files with 268 additions and 11 deletions

View File

@ -11,7 +11,7 @@ export class HTTPPovider {
...customOptions.headers, ...customOptions.headers,
mode: 'cors', mode: 'cors',
credentials: 'include', credentials: 'include',
Authorization: `Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4NXh0WnA5eThxVDBXVDkwUFpuUkRja3N4LWw0clVyM0tHQW5JSU9DckJNIn0.eyJleHAiOjE2ODQ2Njc2MTEsImlhdCI6MTY4NDU4MTIxMSwianRpIjoiN2VlNzQ5ZTMtMjdhOC00ZTc1LWE4MTAtOTU0MGY5NDdmNjlkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9kaXBhbF9kZXYiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiY2RmYzY3YzQtZGJkOC00NGVhLWI0OWEtYjQ3MjZhMzNmOTAxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY29tZm9ydGVjaCIsInNlc3Npb25fc3RhdGUiOiI3YTFlZDk2OS1lNWY2LTQzZTctOThhMy05OGQ3Zjk3YWM1NDgiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiY29tZm9ydGVjaCI6eyJyb2xlcyI6WyJ1c2VyIiwib3BlcmF0b3IiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjdhMWVkOTY5LWU1ZjYtNDNlNy05OGEzLTk4ZDdmOTdhYzU0OCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiKzc3Nzc3Nzc3Nzc3In0.qp1GetUEy2LOZWy6Aaiwf_0d0U8wBqXJQhhuSgIO4RkkEgUnwYZ5fFupkp1iTXZMpqWAsmbRp-C0Z8nYT8Hor6XjnE73XwAVVY0Jbx6HSxtcTBOqo2IT0SmVm6z-TFpgYnErHiFZZgsqP4KYkc12xlQH4SrpN-h-oXN4ZtwuOIG65ixt2yKC-8KTyZzfZGa_8llAtnthQBtxX00MdivFpRP-NU1KfCtJqHSTKn40RNs-Nt8Gi_x7vWv9OKD8h-IIp27oOCJZNyL4aa237cuPw9IWbdiDuUAOgxkPw30i9LIDPA70GvdpRKWgLq0-itcT_hpf2RguuALDafaqoGgoGQ`, Authorization: `Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI4NXh0WnA5eThxVDBXVDkwUFpuUkRja3N4LWw0clVyM0tHQW5JSU9DckJNIn0.eyJleHAiOjE2ODQ4MzM5NzcsImlhdCI6MTY4NDc0NzU3NywianRpIjoiYjE5MDEyNGItYzRjNC00NzlkLThkYWItN2VjODc1MjljZWQyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9kaXBhbF9kZXYiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiY2RmYzY3YzQtZGJkOC00NGVhLWI0OWEtYjQ3MjZhMzNmOTAxIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY29tZm9ydGVjaCIsInNlc3Npb25fc3RhdGUiOiJiYjFmNjc3OC0xMzlhLTRmNzItOWM3Ny01NjAwMWU0NDYzNjQiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1tYXN0ZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiY29tZm9ydGVjaCI6eyJyb2xlcyI6WyJ1c2VyIiwib3BlcmF0b3IiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6ImJiMWY2Nzc4LTEzOWEtNGY3Mi05Yzc3LTU2MDAxZTQ0NjM2NCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiKzc3Nzc3Nzc3Nzc3In0.qJS5_c9g2AADwusGpquWw7zMvc42tzJ0yUMcM6jI6F2MNH2tFDqMhvG0nEnCwXIxJA54DZL8HHPDoxkhq_xyP2SSRKEU-S7pncpa2acNzOYT68pLxBD6s3W-akQxxJVlr92RtegqaHf2BAZMwdMJl4VreX_avPCrEdPzv2dEMX7a2wxteYgzQJsYtaaVyCO4QADMiNVMWgXE00Hnn5Rxuhpe9Y7Kl9cWCO5JY63gYXGFC9yUBEqEYl6o9d6XKMkuiaLJRE2l4k5ycKuJWUjhvCaL7J_f68vJzNhkiuMqmX5q08SDlgktNHzyKTVXkndKz2EpQemzM6SXPLnohPwjAg`,
}, },
}; };
const response = await axios<ApiGlobalResponseObject<R>>(options); const response = await axios<ApiGlobalResponseObject<R>>(options);

View File

@ -0,0 +1,40 @@
import React from 'react';
import ReactDOM from 'react-dom';
export interface notifInterface {
type: 'error' | 'success' | 'warning';
message: string;
time?: number;
onCloseCallback?: () => unknown;
}
export default function Notification({ message, type, time = 5, onCloseCallback }: notifInterface) {
const notifRef = React.useRef<HTMLDivElement>(null);
const el = React.useRef(document.createElement('div'));
React.useEffect(() => {
const portal = document.getElementById('root');
portal?.appendChild(el.current);
setTimeout(() => {
el.current.remove();
if (typeof onCloseCallback !== 'undefined') onCloseCallback();
}, 1000 * time);
return () => {
if (typeof onCloseCallback !== 'undefined') onCloseCallback();
return el.current?.remove();
};
}, []);
return ReactDOM.createPortal(
<div
ref={notifRef}
className={`fixed top-10 left-1/2 translate-x-[-50%] z-30 p-2 rounded-md text-black ${
type === 'error' && 'bg-red-600 text-white'
} ${type === 'success' && 'bg-green-600 text-white'} ${type === 'warning' && 'bg-yellow-500'}`}
>
{message}
</div>,
el.current,
);
}

View File

@ -17,6 +17,7 @@ export const staticMessages = {
phonenumber: 'Phone Number', phonenumber: 'Phone Number',
enterPanel: 'Enter to Panel', enterPanel: 'Enter to Panel',
enterPhoneNumber: 'Enter your phone number', enterPhoneNumber: 'Enter your phone number',
enterOtpCode: 'Enter your Otp Code',
}, },
service: { service: {
errors: { errors: {

View File

@ -0,0 +1,3 @@
import OtpCode from './infra/OtpCode';
export default OtpCode;

View File

@ -0,0 +1,18 @@
/* eslint-disable react/display-name */
import React from 'react';
import OtpCodeView from '../view/OtpCodeView';
import useOtpCodeVm from '../viewmodel/OtpCodeVM';
const OtpCode = React.forwardRef<HTMLInputElement[]>((_props, otpCharRef) => {
const { eventHandlers } = useOtpCodeVm({
otpChar: otpCharRef as unknown as React.MutableRefObject<HTMLInputElement[]>,
});
return (
<OtpCodeView
eventHandlers={eventHandlers}
otpChar={otpCharRef as unknown as React.MutableRefObject<HTMLInputElement[]>}
/>
);
});
export default OtpCode;

View File

@ -0,0 +1,38 @@
/* eslint-disable consistent-return */
/* eslint-disable no-return-assign */
/* eslint-disable react/no-array-index-key */
import React from 'react';
import { staticMessages } from '~/driven/utils/constants/staticMessages';
export interface IOtpCodeView {
otpChar: React.MutableRefObject<HTMLInputElement[]>;
eventHandlers: {
handleFocusInput: (e: React.FocusEvent) => void;
handleKeyPressInput: (e: React.KeyboardEvent) => void;
};
}
export default function OtpCodeView(props: IOtpCodeView) {
const { eventHandlers, otpChar } = props;
const { handleFocusInput, handleKeyPressInput } = eventHandlers;
const otpInputs = Array.from({ length: 6 }).map((digit, i) => (
<input
tabIndex={i + 1}
ref={(el: HTMLInputElement) => (otpChar.current[i] = el)}
key={`otp_char_${i}`}
className='font-bold inline-block w-5 bg-transparent text-center focus:outline-none'
maxLength={1}
defaultValue='_'
placeholder='_'
onClick={(e) => e.stopPropagation()}
onFocus={handleFocusInput}
onKeyDown={handleKeyPressInput}
/>
));
return (
<div className='mb-9 justify-end items-center text-xs self-start'>
<div className='text-xs mb-9 self-start text-txt-medium'>{staticMessages.global.enterOtpCode}</div>
<div className='w-full flex gap-4'>{otpInputs}</div>
</div>
);
}

View File

@ -0,0 +1,114 @@
import { useEffect } from 'react';
import { IOtpCodeView } from '../view/OtpCodeView';
interface IOtpCodeVm {
otpChar: React.MutableRefObject<HTMLInputElement[]>;
}
type useOtpCodeReturnType = Pick<IOtpCodeView, 'eventHandlers'>;
const useOtpCodeVm = (dependencies: IOtpCodeVm): useOtpCodeReturnType => {
const { otpChar } = dependencies;
function focusToInput(target: HTMLInputElement, inputTabIndex: number) {
const input = target.parentElement?.querySelector(`input[tabindex="${inputTabIndex}"]`) as
| HTMLInputElement
| undefined;
if (!input) return;
setTimeout(() => input.focus(), 150);
}
const handleFocusFirstInput = () => {
if (!otpChar.current.length) return;
otpChar.current[0].focus();
};
useEffect(() => {
handleFocusFirstInput();
}, []);
const handleFocusInput = (e: React.FocusEvent) => {
const target = e.target as HTMLInputElement;
// check previous inputs are not empty
const currentIndex = target.getAttribute('tabindex');
if (!currentIndex || +currentIndex === 1) {
target.select();
return;
}
// get first previous empty
let isFindEmptyInput = false;
const firstEmptyInput = otpChar.current.find((item) => {
const otpItemIndex = item.getAttribute('tabindex');
if (!otpItemIndex) return false;
const isInputEmpty = !item.value.trim() || item.value.trim() === '_';
if (+otpItemIndex < +currentIndex && isInputEmpty && !isFindEmptyInput) {
isFindEmptyInput = true;
return true;
}
return false;
});
if (firstEmptyInput) {
firstEmptyInput.select();
return;
}
// focus to it
target.select();
};
function goToPreviousInput(target: HTMLInputElement, currentIndex: number) {
const preIndex = +currentIndex - 1;
if (!preIndex) return;
focusToInput(target, preIndex);
}
function goToNextInput(target: HTMLInputElement, currentIndex: number) {
// get next index
const nextIndex = +currentIndex + 1;
if (!nextIndex) return;
focusToInput(target, nextIndex);
}
const handleKeyPressInput = (e: React.KeyboardEvent) => {
// target
const target = e.target as HTMLInputElement;
// get current index
const currentIndex = target.getAttribute('tabindex');
if (!currentIndex) return;
const isRemoveChar = e.key.toLowerCase() === 'backspace';
if (isRemoveChar && +currentIndex !== 1) {
goToPreviousInput(target, +currentIndex);
return;
}
goToNextInput(target, +currentIndex);
};
return {
eventHandlers: {
handleFocusInput,
handleKeyPressInput,
},
};
};
export default useOtpCodeVm;

View File

@ -1,27 +1,38 @@
import React from 'react'; import React, { useRef, useState } from 'react';
import PrimaryButton from '~/driven/utils/components/buttons/primary-button/PrimaryButton'; import PrimaryButton from '~/driven/utils/components/buttons/primary-button/PrimaryButton';
import SimpleInput from '~/driven/utils/components/inputs/simple-input/SimpleInput';
import { icons } from '~/driven/utils/constants/assertUrls'; import { icons } from '~/driven/utils/constants/assertUrls';
import { staticMessages } from '~/driven/utils/constants/staticMessages'; import { staticMessages } from '~/driven/utils/constants/staticMessages';
import OtpCode from '../otp-code-inputs';
import PhoneNumberAuth from './PhoneNumberAuth';
export default function AuthenticationView() { export default function AuthenticationView() {
const [phoneNumberValue, setPhoneNumberValue] = useState('');
const otpChar = useRef<HTMLInputElement[]>([]);
const statesName = {
phonenumber: <PhoneNumberAuth stateData={{ setState: setPhoneNumberValue, stateValue: phoneNumberValue }} />,
otp: <OtpCode ref={otpChar} />,
};
const [authState, setAuthState] = useState<keyof typeof statesName>('phonenumber');
const submitForm = (e: React.FormEvent) => {
e.preventDefault();
setAuthState('otp');
};
return ( return (
<div className='main-auth flex flex-nowrap justify-start flex-row-reverse h-screen w-screen'> <div className='main-auth flex flex-nowrap justify-start flex-row-reverse h-screen w-screen'>
<form className='w-full px-7 md:px-20 md:w-[50%] lg:w-[35%] min-w-[10rem] h-full shadow-lg shadow-slate-400 flex flex-col items-center justify-start pt-12'> <form
onSubmit={submitForm}
className='w-full px-7 md:px-20 md:w-[50%] lg:w-[35%] min-w-[10rem] h-full shadow-lg shadow-slate-400 flex flex-col items-center justify-start pt-12'
>
<div className='w-48 h-[35%]'> <div className='w-48 h-[35%]'>
<img src={icons.logoBlack} className='w-full h-12' alt='page icon' /> <img src={icons.logoBlack} className='w-full h-12' alt='page icon' />
</div> </div>
<div className='font-normal mb-4 text-lg self-start'>{staticMessages.global.enterPanel}</div> <div className='font-normal mb-4 text-lg self-start'>{staticMessages.global.enterPanel}</div>
<div className='text-txt-medium text-xs mb-9 self-start'>{staticMessages.global.enterPhoneNumber}</div> {statesName[authState]}
<SimpleInput
inputData={{ name: 'phoneNumber', title: staticMessages.global.phonenumber }}
stateHanlder={() => null}
className='mb-9 w-full self-start'
/>
<PrimaryButton <PrimaryButton
onClick={() => null} onClick={() => null}
title={staticMessages.global.submit} title={staticMessages.global.submit}
className='bg-gradient-button w-full h-11' className='[background:var(--color-gradient-button)] hover:brightness-90 transition-all w-full h-11'
/> />
</form> </form>
<div className='hidden md:flex md:w-[50%] lg:w-[65%] h-full' /> <div className='hidden md:flex md:w-[50%] lg:w-[65%] h-full' />

View File

@ -0,0 +1,32 @@
import SimpleInput, { SetStateInputMethod } from '~/driven/utils/components/inputs/simple-input/SimpleInput';
import { staticMessages } from '~/driven/utils/constants/staticMessages';
import { checkPhoneNumberInput } from '~/driven/utils/helpers/globalHelpers';
interface IPhoneNumberAuth {
stateData: {
stateValue: string;
setState: React.Dispatch<React.SetStateAction<string>>;
};
}
export default function PhoneNumberAuth(props: IPhoneNumberAuth) {
const { stateData } = props;
const { setState, stateValue } = stateData;
const onChangeInput: SetStateInputMethod<typeof staticMessages.global.phonenumber> = (
_name: typeof staticMessages.global.phonenumber,
newValue: string,
) => {
if (!checkPhoneNumberInput(newValue)) return;
setState(newValue);
};
return (
<>
<div className='text-txt-medium text-xs mb-9 self-start'>{staticMessages.global.enterPhoneNumber}</div>
<SimpleInput
inputData={{ name: staticMessages.global.phonenumber, title: staticMessages.global.phonenumber }}
stateHanlder={{ setState: onChangeInput, state: stateValue }}
className='mb-9 w-full self-start'
/>
</>
);
}