[FEAT]: add authentication page ui logic
This commit is contained in:
parent
2b38085e42
commit
832220365e
@ -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);
|
||||||
|
40
src/driven/utils/components/Notification/Notification.tsx
Normal file
40
src/driven/utils/components/Notification/Notification.tsx
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
@ -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: {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import OtpCode from './infra/OtpCode';
|
||||||
|
|
||||||
|
export default OtpCode;
|
@ -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;
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
@ -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' />
|
||||||
|
@ -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'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user