From 832220365e8727578f3745ec935319bed44bc16d Mon Sep 17 00:00:00 2001 From: behnamrhp Date: Mon, 22 May 2023 14:34:40 +0300 Subject: [PATCH] [FEAT]: add authentication page ui logic --- .../boundaries/http-boundary/httpBoundary.ts | 2 +- .../components/Notification/Notification.tsx | 40 ++++++ src/driven/utils/constants/staticMessages.ts | 1 + .../authentication/otp-code-inputs/index.tsx | 3 + .../otp-code-inputs/infra/OtpCode.tsx | 18 +++ .../otp-code-inputs/view/OtpCodeView.tsx | 38 ++++++ .../otp-code-inputs/viewmodel/OtpCodeVM.ts | 114 ++++++++++++++++++ .../view/AuthenticationView.tsx | 31 +++-- .../authentication/view/PhoneNumberAuth.tsx | 32 +++++ 9 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 src/driven/utils/components/Notification/Notification.tsx create mode 100644 src/driving/application/generic/authentication/otp-code-inputs/index.tsx create mode 100644 src/driving/application/generic/authentication/otp-code-inputs/infra/OtpCode.tsx create mode 100644 src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx create mode 100644 src/driving/application/generic/authentication/otp-code-inputs/viewmodel/OtpCodeVM.ts create mode 100644 src/driving/application/generic/authentication/view/PhoneNumberAuth.tsx diff --git a/src/driven/boundaries/http-boundary/httpBoundary.ts b/src/driven/boundaries/http-boundary/httpBoundary.ts index 4026b7c..69ff31b 100644 --- a/src/driven/boundaries/http-boundary/httpBoundary.ts +++ b/src/driven/boundaries/http-boundary/httpBoundary.ts @@ -11,7 +11,7 @@ export class HTTPPovider { ...customOptions.headers, mode: 'cors', 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>(options); diff --git a/src/driven/utils/components/Notification/Notification.tsx b/src/driven/utils/components/Notification/Notification.tsx new file mode 100644 index 0000000..9f62aca --- /dev/null +++ b/src/driven/utils/components/Notification/Notification.tsx @@ -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(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( +
+ {message} +
, + el.current, + ); +} diff --git a/src/driven/utils/constants/staticMessages.ts b/src/driven/utils/constants/staticMessages.ts index 6064a1f..6c932d3 100644 --- a/src/driven/utils/constants/staticMessages.ts +++ b/src/driven/utils/constants/staticMessages.ts @@ -17,6 +17,7 @@ export const staticMessages = { phonenumber: 'Phone Number', enterPanel: 'Enter to Panel', enterPhoneNumber: 'Enter your phone number', + enterOtpCode: 'Enter your Otp Code', }, service: { errors: { diff --git a/src/driving/application/generic/authentication/otp-code-inputs/index.tsx b/src/driving/application/generic/authentication/otp-code-inputs/index.tsx new file mode 100644 index 0000000..6162c1a --- /dev/null +++ b/src/driving/application/generic/authentication/otp-code-inputs/index.tsx @@ -0,0 +1,3 @@ +import OtpCode from './infra/OtpCode'; + +export default OtpCode; diff --git a/src/driving/application/generic/authentication/otp-code-inputs/infra/OtpCode.tsx b/src/driving/application/generic/authentication/otp-code-inputs/infra/OtpCode.tsx new file mode 100644 index 0000000..982d05d --- /dev/null +++ b/src/driving/application/generic/authentication/otp-code-inputs/infra/OtpCode.tsx @@ -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((_props, otpCharRef) => { + const { eventHandlers } = useOtpCodeVm({ + otpChar: otpCharRef as unknown as React.MutableRefObject, + }); + return ( + } + /> + ); +}); + +export default OtpCode; diff --git a/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx b/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx new file mode 100644 index 0000000..e8596a0 --- /dev/null +++ b/src/driving/application/generic/authentication/otp-code-inputs/view/OtpCodeView.tsx @@ -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; + 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) => ( + (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 ( +
+
{staticMessages.global.enterOtpCode}
+
{otpInputs}
+
+ ); +} diff --git a/src/driving/application/generic/authentication/otp-code-inputs/viewmodel/OtpCodeVM.ts b/src/driving/application/generic/authentication/otp-code-inputs/viewmodel/OtpCodeVM.ts new file mode 100644 index 0000000..3d05da3 --- /dev/null +++ b/src/driving/application/generic/authentication/otp-code-inputs/viewmodel/OtpCodeVM.ts @@ -0,0 +1,114 @@ +import { useEffect } from 'react'; +import { IOtpCodeView } from '../view/OtpCodeView'; + +interface IOtpCodeVm { + otpChar: React.MutableRefObject; +} + +type useOtpCodeReturnType = Pick; + +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; diff --git a/src/driving/application/generic/authentication/view/AuthenticationView.tsx b/src/driving/application/generic/authentication/view/AuthenticationView.tsx index 75be455..56ad942 100644 --- a/src/driving/application/generic/authentication/view/AuthenticationView.tsx +++ b/src/driving/application/generic/authentication/view/AuthenticationView.tsx @@ -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 SimpleInput from '~/driven/utils/components/inputs/simple-input/SimpleInput'; import { icons } from '~/driven/utils/constants/assertUrls'; import { staticMessages } from '~/driven/utils/constants/staticMessages'; +import OtpCode from '../otp-code-inputs'; +import PhoneNumberAuth from './PhoneNumberAuth'; export default function AuthenticationView() { + const [phoneNumberValue, setPhoneNumberValue] = useState(''); + const otpChar = useRef([]); + const statesName = { + phonenumber: , + otp: , + }; + const [authState, setAuthState] = useState('phonenumber'); + + const submitForm = (e: React.FormEvent) => { + e.preventDefault(); + setAuthState('otp'); + }; return (
-
+
page icon
{staticMessages.global.enterPanel}
-
{staticMessages.global.enterPhoneNumber}
- null} - className='mb-9 w-full self-start' - /> + {statesName[authState]} null} 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' />
diff --git a/src/driving/application/generic/authentication/view/PhoneNumberAuth.tsx b/src/driving/application/generic/authentication/view/PhoneNumberAuth.tsx new file mode 100644 index 0000000..aeac495 --- /dev/null +++ b/src/driving/application/generic/authentication/view/PhoneNumberAuth.tsx @@ -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>; + }; +} +export default function PhoneNumberAuth(props: IPhoneNumberAuth) { + const { stateData } = props; + const { setState, stateValue } = stateData; + const onChangeInput: SetStateInputMethod = ( + _name: typeof staticMessages.global.phonenumber, + newValue: string, + ) => { + if (!checkPhoneNumberInput(newValue)) return; + setState(newValue); + }; + + return ( + <> +
{staticMessages.global.enterPhoneNumber}
+ + + ); +}