feat: register with validation

:%s
This commit is contained in:
lytrankieio123
2021-09-29 12:05:05 +07:00
parent c0e703e1ae
commit 4776ec6236
13 changed files with 589 additions and 264 deletions

View File

@@ -3,38 +3,51 @@ import React, { memo } from 'react'
import s from './ButtonCommon.module.scss'
interface Props {
children?: React.ReactNode,
type?: 'primary' | 'light' | 'ghost' | 'lightBorderNone',
size?: 'default' | 'large' | 'small',
icon?: React.ReactNode,
isIconSuffix?: boolean,
loading?: boolean,
disabled?: boolean,
onClick?: () => void,
children?: React.ReactNode
type?: 'primary' | 'light' | 'ghost' | 'lightBorderNone'
HTMLType?: "submit" | "button" | "reset"
size?: 'default' | 'large' | 'small'
icon?: React.ReactNode
isIconSuffix?: boolean
loading?: boolean
disabled?: boolean
onClick?: () => void
}
const ButtonCommon = memo(({ type = 'primary', size = 'default', loading = false, isIconSuffix = false,
icon, disabled, children, onClick }: Props) => {
const ButtonCommon = memo(
({
type = 'primary',
HTMLType,
size = 'default',
loading = false,
isIconSuffix = false,
icon,
disabled,
children,
onClick,
}: Props) => {
return (
<button className={classNames({
[s.buttonCommon]: true,
[s[type]]: !!type,
[s[size]]: !!size,
[s.loading]: loading,
[s.preserve]: isIconSuffix,
[s.onlyIcon]: icon && !children,
<button
className={classNames({
[s.buttonCommon]: true,
[s[type]]: !!type,
[s[size]]: !!size,
[s.loading]: loading,
[s.preserve]: isIconSuffix,
[s.onlyIcon]: icon && !children,
})}
disabled={disabled || loading}
onClick={onClick}
>
<div className={s.inner}>
{
icon && <span className={s.icon}>{icon}</span>
}
<span className={s.label}>{children}</span>
</div>
</button>
disabled={disabled || loading}
onClick={onClick}
type={HTMLType}
>
<div className={s.inner}>
{icon && <span className={s.icon}>{icon}</span>}
<span className={s.label}>{children}</span>
</div>
</button>
)
})
}
)
ButtonCommon.displayName = 'ButtonCommon'
export default ButtonCommon

View File

@@ -1,100 +1,5 @@
@import "../../../styles/utilities";
@import "../../../styles/form";
.inputWrap {
.inputInner {
@apply flex items-center relative;
.icon {
@apply absolute flex justify-center items-center;
content: "";
left: 1.6rem;
margin-right: 1.6rem;
svg path {
fill: currentColor;
}
}
.icon + .inputCommon {
padding-left: 4.8rem;
}
.inputCommon {
@apply block w-full transition-all duration-200 bg-white;
border-radius: .8rem;
padding: 1.6rem;
border: 1px solid var(--border-line);
&:hover,
&:focus,
&:active {
outline: none;
border: 1px solid var(--primary);
@apply shadow-md;
}
&::placeholder {
@apply text-label;
}
}
&.preserve {
@apply flex-row-reverse;
.icon {
left: unset;
right: 1.6rem;
margin-left: 1.6rem;
margin-right: 0;
svg path {
fill: var(--text-label);
}
}
.icon + .inputCommon {
padding-left: 1.6rem;
padding-right: 4.8rem;
}
}
&.success {
.icon {
svg path {
fill: var(--primary);
}
}
}
&.error {
.icon {
svg path {
fill: var(--negative);
}
}
input {
border-color: var(--negative) !important;
}
}
}
.errorMessage {
@apply caption;
color: var(--negative);
margin-top: 0.4rem;
}
&.custom {
@apply shape-common;
.inputCommon {
border: none;
background: var(--background-gray);
&:hover,
&:focus,
&:active {
@apply shadow-md;
border: none;
}
}
}
&.bgTransparent {
.inputCommon {
background: rgb(227, 242, 233, 0.3);
color: var(--white);
&::placeholder {
color: var(--white);
}
}
}
@extend .formInputWrap;
}

View File

@@ -1,95 +1,127 @@
import classNames from 'classnames';
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
import { IconCheck, IconError } from 'src/components/icons';
import { KEY } from 'src/utils/constanst.utils';
import s from './InputCommon.module.scss';
import classNames from 'classnames'
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'
import { IconCheck, IconError } from 'src/components/icons'
import { KEY } from 'src/utils/constanst.utils'
import s from './InputCommon.module.scss'
type Ref = {
focus: () => void
getValue: () => string | number
} | null;
focus: () => void
getValue: () => string | number
} | null
interface Props {
children?: React.ReactNode,
value?: string | number,
placeholder?: string,
type?: 'text' | 'number' | 'email' | 'password',
styleType?: 'default' | 'custom',
backgroundTransparent?: boolean,
icon?: React.ReactNode,
isIconSuffix?: boolean,
isShowIconSuccess?: boolean,
error?: string,
onChange?: (value: string | number) => void,
onEnter?: (value: string | number) => void,
children?: React.ReactNode
value?: string | number
placeholder?: string
type?: 'text' | 'number' | 'email' | 'password'
styleType?: 'default' | 'custom'
backgroundTransparent?: boolean
icon?: React.ReactNode
isIconSuffix?: boolean
isShowIconSuccess?: boolean
error?: string
onChange?: (value: string | number) => void
onChangeEvent?: (e: React.ChangeEvent<HTMLInputElement>) => void
onBlur?: (e: any) => void
onEnter?: (value: string | number) => void
}
const InputCommon = forwardRef<Ref, Props>(({ value, placeholder, type, styleType = 'default', icon, backgroundTransparent = false,
isIconSuffix, isShowIconSuccess, error,
onChange, onEnter }: Props, ref) => {
const inputElementRef = useRef<HTMLInputElement>(null);
const InputCommon = forwardRef<Ref, Props>(
(
{
value,
placeholder,
type,
styleType = 'default',
icon,
backgroundTransparent = false,
isIconSuffix,
isShowIconSuccess,
error,
onChange,
onChangeEvent,
onEnter,
onBlur,
}: Props,
ref
) => {
const inputElementRef = useRef<HTMLInputElement>(null)
const iconElement = useMemo(() => {
if (error) {
return <span className={s.icon}><IconError /> </span>
} else if (isShowIconSuccess) {
return <span className={s.icon}><IconCheck /> </span>
} else if (icon) {
return <span className={s.icon}>{icon} </span>
}
return <></>
if (error) {
return (
<span className={s.icon}>
<IconError />{' '}
</span>
)
} else if (isShowIconSuccess) {
return (
<span className={s.icon}>
<IconCheck />{' '}
</span>
)
} else if (icon) {
return <span className={s.icon}>{icon} </span>
}
return <></>
}, [icon, error, isShowIconSuccess])
useImperativeHandle(ref, () => ({
focus: () => {
inputElementRef.current?.focus();
},
getValue: () => {
const value = inputElementRef.current?.value || ''
return value
}
}));
focus: () => {
inputElementRef.current?.focus()
},
getValue: () => {
const value = inputElementRef.current?.value || ''
return value
},
}))
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onChangeEvent) {
onChangeEvent(e)
} else {
onChange && onChange(e.target.value)
}
}
const handleKeyDown = (e: any) => {
if (e.key === KEY.ENTER && onEnter) {
const value = inputElementRef.current?.value || ''
onEnter(value)
}
if (e.key === KEY.ENTER && onEnter) {
const value = inputElementRef.current?.value || ''
onEnter(value)
}
}
return (
<div className={classNames({
[s.inputWrap]: true,
[s[styleType]]: true,
[s.bgTransparent]: backgroundTransparent
})}>
<div className={classNames({
[s.inputInner]: true,
[s.preserve]: isIconSuffix,
[s.success]: isShowIconSuccess,
[s.error]: !!error,
})}>
{iconElement}
<input
ref={inputElementRef}
value={value}
type={type}
placeholder={placeholder}
onChange={handleChange}
onKeyDown={handleKeyDown}
className={s.inputCommon}
/>
</div>
{
error && <div className={s.errorMessage}>{error}</div>
}
<div
className={classNames({
[s.inputWrap]: true,
[s[styleType]]: true,
[s.bgTransparent]: backgroundTransparent,
})}
>
<div
className={classNames({
[s.inputInner]: true,
[s.preserve]: isIconSuffix,
[s.success]: isShowIconSuccess,
[s.error]: !!error,
})}
>
{iconElement}
<input
ref={inputElementRef}
value={value}
type={type}
placeholder={placeholder}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={onBlur}
/>
</div>
{error && <div className={s.errorMessage}>{error}</div>}
</div>
)
}
)
})
InputCommon.displayName = 'InputCommon'
export default InputCommon

View File

@@ -0,0 +1,5 @@
@import "../../../styles/form";
.inputWrap {
@extend .formInputWrap;
}

View File

@@ -0,0 +1,94 @@
import classNames from 'classnames'
import { Field } from 'formik'
import React, { useMemo, useRef } from 'react'
import { IconCheck, IconError } from 'src/components/icons'
import { KEY } from 'src/utils/constanst.utils'
import s from './InputFiledInForm.module.scss'
interface Props {
placeholder?: string
type?: 'text' | 'number' | 'email' | 'password'
styleType?: 'default' | 'custom'
backgroundTransparent?: boolean
icon?: React.ReactNode
isIconSuffix?: boolean
isShowIconSuccess?: boolean
name: string
error?: string
onChange?: (value: string | number) => void
onChangeEvent?: (e: React.ChangeEvent<HTMLInputElement>) => void
onBlur?: (e: any) => void
onEnter?: (value: string | number) => void
}
const InputFiledInForm = ({
name,
placeholder,
type,
styleType = 'default',
icon,
backgroundTransparent = false,
isIconSuffix = true,
isShowIconSuccess,
error,
onEnter,
}: Props) => {
const inputElementRef = useRef<HTMLInputElement>(null)
const iconElement = useMemo(() => {
if (error) {
return (
<span className={s.icon}>
<IconError />{' '}
</span>
)
} else if (isShowIconSuccess) {
return (
<span className={s.icon}>
<IconCheck />{' '}
</span>
)
} else if (icon) {
return <span className={s.icon}>{icon} </span>
}
return <></>
}, [icon, error, isShowIconSuccess])
const handleKeyDown = (e: any) => {
if (e.key === KEY.ENTER && onEnter) {
const value = inputElementRef.current?.value || ''
onEnter(value)
}
}
return (
<div
className={classNames({
[s.inputWrap]: true,
[s[styleType]]: true,
[s.bgTransparent]: backgroundTransparent,
})}
>
<div
className={classNames({
[s.inputInner]: true,
[s.preserve]: isIconSuffix,
[s.success]: isShowIconSuccess,
[s.error]: !!error,
})}
>
{iconElement}
<Field
name={name}
placeholder={placeholder}
onKeyDown={handleKeyDown}
type={type}
/>
</div>
{error && <div className={s.errorMessage}>{error}</div>}
</div>
)
}
InputFiledInForm.displayName = 'InputFiledInForm'
export default InputFiledInForm

View File

@@ -0,0 +1,10 @@
.iconPassword {
all: unset;
cursor: pointer;
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
}

View File

@@ -0,0 +1,49 @@
import React, { useState } from 'react'
import { IconPassword, IconPasswordCross } from 'src/components/icons'
import InputFiledInForm from '../InputFiledInForm/InputFiledInForm'
import s from './InputPasswordFiledInForm.module.scss'
interface Props {
name?: string
placeholder?: string
styleType?: 'default' | 'custom'
error?: string
onChange?: (value: string | number) => void
onEnter?: (value: string | number) => void
}
const InputPasswordFiledInForm = ({
name = 'password',
placeholder,
styleType = 'default',
error,
onChange,
onEnter,
}: Props) => {
const [isShowPassword, setIsShowPassword] = useState<boolean>(false)
const toggleShowPassword = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
setIsShowPassword(!isShowPassword)
}
return (
<InputFiledInForm
name={name}
type={isShowPassword ? 'text' : 'password'}
styleType={styleType}
error={error}
placeholder={placeholder}
icon={
<button className={s.iconPassword} onClick={toggleShowPassword}>
{isShowPassword ? <IconPassword /> : <IconPasswordCross />}
</button>
}
isIconSuffix={true}
onChange={onChange}
onEnter={onEnter}
/>
)
}
export default InputPasswordFiledInForm

View File

@@ -1,83 +1,121 @@
import classNames from 'classnames'
import React, { useEffect, useRef, useState } from 'react'
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common'
import { Form, Formik } from 'formik'
import React, { useEffect, useRef } from 'react'
import {
ButtonCommon,
InputFiledInForm,
InputPasswordFiledInForm
} from 'src/components/common'
import { CustomInputCommon } from 'src/utils/type.utils'
import * as Yup from 'yup'
import { useSignup } from '../../../../hooks'
import s from '../FormAuthen.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen'
import styles from './FormRegister.module.scss'
interface Props {
isHide: boolean,
onSwitch: () => void
isHide: boolean
onSwitch: () => void
}
const DisplayingErrorMessagesSchema = Yup.object().shape({
email: Yup.string().email('Your email was wrong').required('Required'),
password: Yup.string()
.matches(
/^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])((?=.*[0-9!@#$%^&*()\-_=+{};:,<.>]){1}).*$/,
'Must contain 8 characters with at least 1 uppercase and 1 lowercase letter and either 1 number or 1 special character.'
)
.max(30, 'Password is too long')
.required('Required'),
})
const FormRegister = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null)
const { loading, signup, error } = useSignup()
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
const emailRef = useRef<CustomInputCommon>(null)
const { loading, signup, error } = useSignup()
useEffect(() => {
if (!isHide) {
emailRef.current?.focus()
}
}, [isHide])
const onSignup = () => {
// TODO: validate fields
signup({ email, password })
// TODO:
alert("User created. Please verify your email")
useEffect(() => {
if (!isHide) {
emailRef.current?.focus()
}
}, [isHide])
const onSignup = (values: { email: string; password: string }) => {
signup({ email: values.email, password: values.password })
// TODO: flow
alert('User created. Please verify your email')
}
useEffect(() => {
if (error) {
alert(error.message)
}
}, [error])
useEffect(() => {
if (error) {
alert(error.message)
}
}, [error])
return (
<section className={classNames({
[s.formAuthen]: true,
[styles.formRegister]: true,
})}>
<div className={s.inner}>
<div className={s.body}>
<Inputcommon
placeholder='Email Address'
type='email'
ref={emailRef}
value={email}
onChange={(val) => setEmail(val.toString())}
/>
<InputPassword
placeholder='Password'
value={password}
onChange={(val) => setPassword(val.toString())}
/>
<div className={styles.passwordNote}>
Must contain 8 characters with at least 1 uppercase and 1 lowercase letter and either 1 number or 1 special character.
</div>
return (
<section
className={classNames({
[s.formAuthen]: true,
[styles.formRegister]: true,
})}
>
<div className={s.inner}>
<div className={s.body}>
<Formik
initialValues={{
password: '',
email: '',
}}
validationSchema={DisplayingErrorMessagesSchema}
onSubmit={onSignup}
>
{({ errors, touched }) => (
<Form className="u-form">
<div className="body">
<InputFiledInForm
name="email"
placeholder="Email Address"
error={
touched.email && errors.email
? errors.email.toString()
: ''
}
isShowIconSuccess={touched.email && !errors.email}
/>
<InputPasswordFiledInForm
name="password"
placeholder="Password"
error={
touched.password && errors.password
? errors.password.toString()
: ''
}
/>
<div className={styles.passwordNote}>
Must contain 8 characters with at least 1 uppercase and 1
lowercase letter and either 1 number or 1 special character.
</div>
</div>
<div className={styles.bottom}>
<ButtonCommon size='large'
loading={loading}
onClick={onSignup}>
Create Account
</ButtonCommon>
<ButtonCommon
HTMLType="submit"
size="large"
loading={loading}
>
Create Account
</ButtonCommon>
</div>
<SocialAuthen />
<div className={s.others}>
<span>Already an account?</span>
<button onClick={onSwitch}>Sign In</button>
</div>
</div>
</section>
)
</Form>
)}
</Formik>
</div>
<SocialAuthen />
<div className={s.others}>
<span>Already an account?</span>
<button onClick={onSwitch}>Sign In</button>
</div>
</div>
</section>
)
}
export default FormRegister
export default FormRegister

View File

@@ -1,3 +1,4 @@
import { InputFiledInForm } from 'src/components/common/InputFiledInForm/InputFiledInForm';
export { default as ButtonCommon } from './ButtonCommon/ButtonCommon'
export { default as Layout } from './Layout/Layout'
export { default as CarouselCommon } from './CarouselCommon/CarouselCommon'
@@ -47,4 +48,6 @@ export { default as StaticImage} from './StaticImage/StaticImage'
export { default as EmptyCommon} from './EmptyCommon/EmptyCommon'
export { default as CustomShapeSvg} from './CustomShapeSvg/CustomShapeSvg'
export { default as RecommendedRecipes} from './RecommendedRecipes/RecommendedRecipes'
export { default as LayoutCheckout} from './LayoutCheckout/LayoutCheckout'
export { default as LayoutCheckout} from './LayoutCheckout/LayoutCheckout'
export { default as InputPasswordFiledInForm} from './InputPasswordFiledInForm/InputPasswordFiledInForm'
export { default as InputFiledInForm} from './InputFiledInForm/InputFiledInForm'

100
src/styles/_form.scss Normal file
View File

@@ -0,0 +1,100 @@
@import './utilities';
.formInputWrap {
.inputInner {
@apply flex items-center relative;
.icon {
@apply absolute flex justify-center items-center;
content: "";
left: 1.6rem;
margin-right: 1.6rem;
svg path {
fill: currentColor;
}
}
.icon + input {
padding-left: 4.8rem;
}
input {
@apply block w-full transition-all duration-200 bg-white;
border-radius: .8rem;
padding: 1.6rem;
border: 1px solid var(--border-line);
&:hover,
&:focus,
&:active {
outline: none;
border: 1px solid var(--primary);
@apply shadow-md;
}
&::placeholder {
@apply text-label;
}
}
&.preserve {
@apply flex-row-reverse;
.icon {
left: unset;
right: 1.6rem;
margin-left: 1.6rem;
margin-right: 0;
svg path {
fill: var(--text-label);
}
}
.icon + input {
padding-left: 1.6rem;
padding-right: 4.8rem;
}
}
&.success {
.icon {
svg path {
fill: var(--primary);
}
}
}
&.error {
.icon {
svg path {
fill: var(--negative);
}
}
input {
border-color: var(--negative) !important;
}
}
}
.errorMessage {
@apply caption;
color: var(--negative);
margin-top: 0.4rem;
}
&.custom {
@apply shape-common;
input {
border: none;
background: var(--background-gray);
&:hover,
&:focus,
&:active {
@apply shadow-md;
border: none;
}
}
}
&.bgTransparent {
input {
background: rgb(227, 242, 233, 0.3);
color: var(--white);
&::placeholder {
color: var(--white);
}
}
}
}

View File

@@ -6,4 +6,5 @@
@import "~tailwindcss/utilities";
@import './utilities';
@import './form';
@import './pages'