Merge pull request #69 from KieIO/feature/m1-authen

Feature/m1 authen
This commit is contained in:
lytrankieio123
2021-09-29 18:02:34 +07:00
committed by GitHub
58 changed files with 1521 additions and 562 deletions

View File

@@ -3095,6 +3095,36 @@ export type LoginMutation = { __typename?: 'Mutation' } & {
>) >)
} }
export type VerifyCustomerAccountVariables = Exact<{
token: Scalars['String']
password?: Maybe<Scalars['String']>
}>
export type VerifyCustomerAccountMutation = { __typename?: 'Mutation' } & {
verifyCustomerAccount:
| ({ __typename: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
| ({ __typename: 'VerificationTokenInvalidError' } & Pick<
VerificationTokenInvalidError,
'errorCode' | 'message'
>)
| ({ __typename: 'VerificationTokenExpiredError' } & Pick<
VerificationTokenExpiredError,
'errorCode' | 'message'
>)
| ({ __typename: 'MissingPasswordError' } & Pick<
MissingPasswordError,
'errorCode' | 'message'
>)
| ({ __typename: 'PasswordAlreadySetError' } & Pick<
PasswordAlreadySetError,
'errorCode' | 'message'
>)
| ({ __typename: 'NativeAuthStrategyError' } & Pick<
NativeAuthStrategyError,
'errorCode' | 'message'
>)
}
export type LogoutMutationVariables = Exact<{ [key: string]: never }> export type LogoutMutationVariables = Exact<{ [key: string]: never }>
export type LogoutMutation = { __typename?: 'Mutation' } & { export type LogoutMutation = { __typename?: 'Mutation' } & {

View File

@@ -29,6 +29,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-next": "^11.1.2", "eslint-config-next": "^11.1.2",
"formik": "^2.2.9",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
@@ -51,7 +52,8 @@
"swr": "^0.5.6", "swr": "^0.5.6",
"tabbable": "^5.2.0", "tabbable": "^5.2.0",
"tailwindcss": "^2.2.2", "tailwindcss": "^2.2.2",
"uuidv4": "^6.2.10" "uuidv4": "^6.2.10",
"yup": "^0.32.9"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^1.21.5", "@graphql-codegen/cli": "^1.21.5",

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { Layout } from 'src/components/common';
import { AccountSignIn } from 'src/components/modules/account';
const AccountNotLogin = () => {
return (
<>
<AccountSignIn/>
</>
);
};
AccountNotLogin.Layout = Layout
export default AccountNotLogin;

View File

@@ -1,13 +1,16 @@
import React from 'react'; import React from 'react'
import { Layout } from 'src/components/common'; import { Layout } from 'src/components/common'
import { AccountPage } from 'src/components/modules/account'; import useActiveCustomer from 'src/components/hooks/useActiveCustomer'
import { AccountPage, AccountSignIn } from 'src/components/modules/account'
const Account = () => { const Account = () => {
return ( const { customer } = useActiveCustomer()
<AccountPage/> if (customer) {
); return <AccountPage />
}; }
return <AccountSignIn />
}
Account.Layout = Layout Account.Layout = Layout
export default Account; export default Account

View File

@@ -1,85 +1,16 @@
import { import { Layout } from 'src/components/common'
FeaturedProductCard, import { useMessage } from 'src/components/contexts'
Layout
} from 'src/components/common';
import { HomeBanner } from 'src/components/modules/home';
// import { RecipeListPage } from 'src/components/modules/recipes';
import { OPTION_ALL, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils';
import { PRODUCT_DATA_TEST, PRODUCT_DATA_TEST_PAGE } from 'src/utils/demo-data';
const CATEGORY = [
{
name: 'All',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=${OPTION_ALL}`,
},
{
name: 'Veggie',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=veggie`,
},
{
name: 'Seafood',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=seafood`,
},
{
name: 'Frozen',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=frozen`,
},
{
name: 'Coffee Bean',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=coffee-bean`,
},
{
name: 'Sauce',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=sauce`,
},
]
const BRAND = [
{
name: 'Maggi',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=veggie`,
},
{
name: 'Cholimes',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=seafood`,
},
{
name: 'Chinsu',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=frozen`,
}]
const FEATURED = [
{
name: 'Best Sellers',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=veggie`,
},
{
name: 'Sales',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=seafood`,
},
{
name: 'New Item',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=frozen`,
},
{
name: 'Viewed',
link: `${ROUTE.PRODUCTS}/?${QUERY_KEY.BRAND}=viewed`,
}
];
const data = PRODUCT_DATA_TEST[0]
export default function Test() { export default function Test() {
const { showMessageError } = useMessage()
const handleClick = () => {
showMessageError("Create account successfully")
}
return ( return (
<> <>
<FeaturedProductCard <button onClick={handleClick}>Click me</button>
imageSrc={data.imageSrc}
title="Sale 25% coffee bean"
subTitle="50 first orders within a day"
price={data.price}
originPrice="$20.00" />
<HomeBanner/>
</> </>
) )
} }

8
pages/verify.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { Layout } from 'src/components/common'
import { VerifyCustomerAccount } from 'src/components/modules/verify-customer'
export default function VerifyCustomer() {
return <VerifyCustomerAccount />
}
VerifyCustomer.Layout = Layout

View File

@@ -41,4 +41,5 @@ const Banner = memo(({ data }: Props) => {
) )
}) })
Banner.displayName = 'Banner'
export default Banner export default Banner

View File

@@ -2,6 +2,32 @@
.buttonCommon { .buttonCommon {
@apply shape-common; @apply shape-common;
&:hover {
.inner {
@apply shadow-md;
&:not(:disabled) {
filter: brightness(1.05);
}
}
}
&:disabled {
cursor: not-allowed;
.inner {
filter: brightness(0.8) !important;
color: var(--disabled);
}
}
&:focus {
outline: none;
.inner {
filter: brightness(1.05);
}
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
.inner { .inner {
padding: 1rem 2rem; padding: 1rem 2rem;
@apply bg-primary transition-all duration-200 text-white font-bold; @apply bg-primary transition-all duration-200 text-white font-bold;
@@ -14,37 +40,19 @@
@screen lg { @screen lg {
padding: 1.6rem 3.2rem; padding: 1.6rem 3.2rem;
} }
&:disabled {
filter: brightness(0.9);
cursor: not-allowed;
color: var(--disabled);
}
&:hover {
@apply shadow-md;
&:not(:disabled) {
filter: brightness(1.05);
}
}
&:focus {
outline: none;
filter: brightness(1.05);
}
&:focus-visible {
outline: 2px solid var(--text-active);
}
} }
&.loading { &.loading {
.inner { .inner {
&::after { &::after {
content: ""; content: "";
border-radius: 50%; border-radius: 50%;
width: 1.6rem; width: 1.8rem;
height: 1.6rem; height: 1.8rem;
border: 3px solid rgba(170, 170, 170, 0.5); border: 3px solid rgba(170, 170, 170, 0.5);
border-top: 3px solid var(--white); border-top: 3px solid var(--white);
-webkit-animation: spin 2s linear infinite; -webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite; animation: spin 2s linear infinite;
margin-right: 0.8rem; margin-left: 0.8rem;
} }
} }
} }
@@ -60,7 +68,7 @@
&.loading { &.loading {
.inner { .inner {
&::after { &::after {
border-top-color: var(--primary); border-top-color: var(--text-active);
} }
} }
} }
@@ -87,7 +95,7 @@
} }
&.loading { &.loading {
.inner::after { .inner::after {
border-top-color: var(--text-active); border-top-color: var(--primary);
} }
} }
} }
@@ -105,14 +113,14 @@
} }
&.small { &.small {
.inner { .inner {
padding: .5rem 1rem; padding: 0.5rem 1rem;
&.onlyIcon { &.onlyIcon {
padding: 1rem; padding: 1rem;
} }
@screen md { @screen md {
padding: .8rem 1.6rem; padding: 0.8rem 1.6rem;
&.onlyIcon { &.onlyIcon {
padding: .8rem; padding: 0.8rem;
} }
} }
} }

View File

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

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames' import classNames from 'classnames'
import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import React, { memo, useEffect, useRef, useState } from 'react'
import { useModalCommon } from 'src/components/hooks' import { useModalCommon } from 'src/components/hooks'
import { CartDrawer } from '..'
import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate' import ModalAuthenticate from '../ModalAuthenticate/ModalAuthenticate'
import ModalCreateUserInfo from '../ModalCreateUserInfo/ModalCreateUserInfo' import ModalCreateUserInfo from '../ModalCreateUserInfo/ModalCreateUserInfo'
import HeaderHighLight from './components/HeaderHighLight/HeaderHighLight' import HeaderHighLight from './components/HeaderHighLight/HeaderHighLight'
@@ -17,6 +16,7 @@ interface props {
const Header = memo(({ toggleFilter, visibleFilter }: props) => { const Header = memo(({ toggleFilter, visibleFilter }: props) => {
const headeFullRef = useRef<HTMLDivElement>(null) const headeFullRef = useRef<HTMLDivElement>(null)
const [isFullHeader, setIsFullHeader] = useState<boolean>(true) const [isFullHeader, setIsFullHeader] = useState<boolean>(true)
const [isModeAuthenRegister, setIsModeAuthenRegister] = useState<boolean>(false)
const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false }) const { visible: visibleModalAuthen, closeModal: closeModalAuthen, openModal: openModalAuthen } = useModalCommon({ initialValue: false })
const { visible: visibleModalInfo, closeModal: closeModalInfo, openModal: openModalInfo } = useModalCommon({ initialValue: false }) const { visible: visibleModalInfo, closeModal: closeModalInfo, openModal: openModalInfo } = useModalCommon({ initialValue: false })
@@ -32,7 +32,17 @@ const Header = memo(({ toggleFilter, visibleFilter }: props) => {
return () => { return () => {
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
} }
}, [headeFullRef.current]) }, [])
const openModalRegister = () => {
setIsModeAuthenRegister(true)
openModalAuthen()
}
const openModalLogin = () => {
setIsModeAuthenRegister(false)
openModalAuthen()
}
return ( return (
<> <>
@@ -43,7 +53,8 @@ const Header = memo(({ toggleFilter, visibleFilter }: props) => {
<HeaderMenu <HeaderMenu
isStickyHeader={true} isStickyHeader={true}
toggleFilter={toggleFilter} toggleFilter={toggleFilter}
openModalAuthen={openModalAuthen} openModalLogin={openModalLogin}
openModalRegister={openModalRegister}
openModalInfo={openModalInfo} /> openModalInfo={openModalInfo} />
</div> </div>
@@ -54,16 +65,17 @@ const Header = memo(({ toggleFilter, visibleFilter }: props) => {
isFull={isFullHeader} isFull={isFullHeader}
visibleFilter={visibleFilter} visibleFilter={visibleFilter}
toggleFilter={toggleFilter} toggleFilter={toggleFilter}
openModalAuthen={openModalAuthen} openModalLogin={openModalLogin}
openModalInfo={openModalInfo} /> openModalRegister = {openModalRegister}
openModalInfo={openModalInfo}
/>
<HeaderSubMenu /> <HeaderSubMenu />
</div> </div>
</header> </header>
<HeaderSubMenuMobile /> <HeaderSubMenuMobile />
<ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} /> <ModalAuthenticate visible={visibleModalAuthen} closeModal={closeModalAuthen} mode={isModeAuthenRegister? 'register': ''} />
<ModalCreateUserInfo demoVisible={visibleModalInfo} demoCloseModal={closeModalInfo} /> <ModalCreateUserInfo demoVisible={visibleModalInfo} demoCloseModal={closeModalInfo} />
</> </>
) )
}) })

View File

@@ -6,115 +6,164 @@ import { ButtonCommon } from 'src/components/common'
import InputSearch from 'src/components/common/InputSearch/InputSearch' import InputSearch from 'src/components/common/InputSearch/InputSearch'
import MenuDropdown from 'src/components/common/MenuDropdown/MenuDropdown' import MenuDropdown from 'src/components/common/MenuDropdown/MenuDropdown'
import { useCartDrawer } from 'src/components/contexts' import { useCartDrawer } from 'src/components/contexts'
import { IconBuy, IconFilter, IconHeart, IconHistory, IconUser } from 'src/components/icons' import {
import { ACCOUNT_TAB, FILTER_PAGE, QUERY_KEY, ROUTE } from 'src/utils/constanst.utils' IconBuy,
IconFilter,
IconHeart,
IconHistory,
IconUser,
} from 'src/components/icons'
import {
ACCOUNT_TAB,
FILTER_PAGE,
QUERY_KEY,
ROUTE,
} from 'src/utils/constanst.utils'
import Logo from '../../../Logo/Logo' import Logo from '../../../Logo/Logo'
import s from './HeaderMenu.module.scss' import s from './HeaderMenu.module.scss'
import { useLogout } from '../../../../hooks/auth'
import useActiveCustomer from 'src/components/hooks/useActiveCustomer'
interface Props { interface Props {
children?: any, children?: any
isFull?: boolean, isFull?: boolean
isStickyHeader?: boolean, isStickyHeader?: boolean
visibleFilter?: boolean, visibleFilter?: boolean
openModalAuthen: () => void, openModalLogin: () => void
openModalInfo: () => void, openModalRegister: () => void
toggleFilter: () => void, openModalInfo: () => void
toggleFilter: () => void
} }
const HeaderMenu = memo(
const HeaderMenu = memo(({ isFull, isStickyHeader, visibleFilter, openModalAuthen, openModalInfo, toggleFilter }: Props) => { ({
isFull,
isStickyHeader,
visibleFilter,
openModalLogin,
openModalRegister,
openModalInfo,
toggleFilter,
}: Props) => {
const router = useRouter() const router = useRouter()
const { toggleCartDrawer } = useCartDrawer() const { toggleCartDrawer } = useCartDrawer()
const { customer } = useActiveCustomer()
const optionMenu = useMemo(() => [ const { logout } = useLogout()
{
onClick: openModalAuthen,
name: 'Login (Demo)',
},
{
onClick: openModalInfo,
name: 'Create User Info (Demo)',
},
{
link: '/account-not-login',
name: 'Account Not Login (Demo)',
},
{
link: '/demo',
name: 'Notifications Empty (Demo)',
},
{
link: ROUTE.NOTIFICATION,
name: 'Notifications',
},
{
link: ROUTE.ACCOUNT,
name: 'Account',
},
{
link: '/',
name: 'Logout',
},
], [openModalAuthen]) const optionMenuNotAuthen = useMemo(
return ( () => [
<section className={classNames({ {
[s.headerMenu]: true, onClick: openModalLogin,
[s.small]: isStickyHeader, name: 'Sign in',
[s.full]: isFull, },
})}> {
<div className={s.left}> onClick: openModalRegister,
<div className={s.top}> name: 'Create account',
<Logo /> },
<div className={s.iconGroup}> ],
{ [openModalLogin, openModalRegister]
FILTER_PAGE.includes(router.pathname) && (
<button className={s.iconFilter} onClick={toggleFilter}>
<IconFilter />
<div className={classNames({ [s.dot]: true, [s.isShow]: visibleFilter })}></div>
</button>
)
}
<button className={`${s.iconCart} ${s.btnCart}`} onClick={toggleCartDrawer}>
<IconBuy />
</button>
</div>
</div>
<div className={s.searchWrap}>
<div className={s.inputSearch}>
<InputSearch />
</div>
<div className={s.buttonSearch}>
<ButtonCommon>Search</ButtonCommon>
</div>
</div>
</div>
<ul className={s.menu}>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.ORDER}`}>
<a>
<IconHistory />
</a>
</Link>
</li>
<li>
<Link href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`}>
<a className={s.iconFavourite}>
<IconHeart />
</a>
</Link>
</li>
<li>
<MenuDropdown options={optionMenu} isHasArrow={false}><IconUser /></MenuDropdown>
</li>
<li>
<button className={s.btnCart} onClick={toggleCartDrawer}>
<IconBuy />
</button>
</li>
</ul>
</section>
) )
})
const optionMenu = useMemo(
() => [
// {
// onClick: openModalInfo,
// name: 'Create User Info (Demo)',
// },
{
link: '/demo',
name: 'Notifications Empty (Demo)',
},
{
link: ROUTE.NOTIFICATION,
name: 'Notifications',
},
{
link: ROUTE.ACCOUNT,
name: 'Account',
},
{
link: '/',
name: 'Logout',
onClick: logout,
},
],
[logout]
)
return (
<section
className={classNames({
[s.headerMenu]: true,
[s.small]: isStickyHeader,
[s.full]: isFull,
})}
>
<div className={s.left}>
<div className={s.top}>
<Logo />
<div className={s.iconGroup}>
{FILTER_PAGE.includes(router.pathname) && (
<button className={s.iconFilter} onClick={toggleFilter}>
<IconFilter />
<div
className={classNames({
[s.dot]: true,
[s.isShow]: visibleFilter,
})}
></div>
</button>
)}
<button
className={`${s.iconCart} ${s.btnCart}`}
onClick={toggleCartDrawer}
>
<IconBuy />
</button>
</div>
</div>
<div className={s.searchWrap}>
<div className={s.inputSearch}>
<InputSearch />
</div>
<div className={s.buttonSearch}>
<ButtonCommon>Search</ButtonCommon>
</div>
</div>
</div>
<ul className={s.menu}>
<li>
<Link
href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.ORDER}`}
>
<a>
<IconHistory />
</a>
</Link>
</li>
<li>
<Link
href={`${ROUTE.ACCOUNT}?${QUERY_KEY.TAB}=${ACCOUNT_TAB.FAVOURITE}`}
>
<a className={s.iconFavourite}>
<IconHeart />
</a>
</Link>
</li>
<li>
<MenuDropdown options={customer ? optionMenu : optionMenuNotAuthen} isHasArrow={false}>
<IconUser />
</MenuDropdown>
</li>
<li>
<button className={s.btnCart} onClick={toggleCartDrawer}>
<IconBuy />
</button>
</li>
</ul>
</section>
)
}
)
HeaderMenu.displayName = 'HeaderMenu'
export default HeaderMenu export default HeaderMenu

View File

@@ -1,100 +1,5 @@
@import "../../../styles/utilities"; @import "../../../styles/form";
.inputWrap { .inputWrap {
.inputInner { @extend .formInputWrap;
@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);
}
}
}
} }

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
import classNames from 'classnames'
import { Field } from 'formik'
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 './InputFiledInForm.module.scss'
type Ref = {
focus: () => void
} | null
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 = forwardRef<Ref, Props>(({
name,
placeholder,
type,
styleType = 'default',
icon,
backgroundTransparent = false,
isIconSuffix = true,
isShowIconSuccess,
error,
onEnter,
}: Props, ref) => {
const inputElementRef = useRef<HTMLInputElement>(null)
useImperativeHandle(ref, () => ({
focus: () => {
inputElementRef.current?.focus()
},
}))
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: React.KeyboardEvent) => {
if (e.key === KEY.ENTER) {
e.stopPropagation()
e.preventDefault()
if (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}
innerRef={inputElementRef}
/>
</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,25 +1,24 @@
import { CommerceProvider } from '@framework' import { CommerceProvider } from '@framework'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FC } from 'react' import { FC } from 'react'
import { CartDrawerProvider } from 'src/components/contexts' import { CartDrawerProvider, MessageProvider } from 'src/components/contexts'
import LayoutContent from './LayoutContent/LayoutContent' import LayoutContent from './LayoutContent/LayoutContent'
interface Props { interface Props {
className?: string className?: string
children?: any children?: any
} }
const Layout: FC<Props> = ({ children }) => { const Layout: FC<Props> = ({ children }) => {
const { locale = 'en-US' } = useRouter() const { locale = 'en-US' } = useRouter()
return ( return (
<CommerceProvider locale={locale}> <CommerceProvider locale={locale}>
<CartDrawerProvider> <CartDrawerProvider>
<LayoutContent> <MessageProvider>
{children} <LayoutContent>{children}</LayoutContent>
</LayoutContent> </MessageProvider>
</CartDrawerProvider> </CartDrawerProvider>
</CommerceProvider> </CommerceProvider>
)
)
} }
export default Layout export default Layout

View File

@@ -1,8 +1,9 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { FC } from 'react' import { FC } from 'react'
import { useMessage } from 'src/components/contexts'
import { useModalCommon } from 'src/components/hooks' import { useModalCommon } from 'src/components/hooks'
import { BRAND, CATEGORY, FEATURED, FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils' import { BRAND, CATEGORY, FEATURED, FILTER_PAGE, ROUTE } from 'src/utils/constanst.utils'
import { CartDrawer, Footer, ScrollToTop } from '../..' import { CartDrawer, Footer, MessageCommon, ScrollToTop } from '../..'
import Header from '../../Header/Header' import Header from '../../Header/Header'
import MenuNavigationProductList from '../../MenuNavigationProductList/MenuNavigationProductList' import MenuNavigationProductList from '../../MenuNavigationProductList/MenuNavigationProductList'
import s from './LayoutContent.module.scss' import s from './LayoutContent.module.scss'
@@ -16,6 +17,7 @@ const LayoutContent: FC<Props> = ({ children }) => {
const { pathname } = useRouter() const { pathname } = useRouter()
const { visible: visibleFilter, openModal: openFilter, closeModal: closeFilter } = useModalCommon({ initialValue: false }) const { visible: visibleFilter, openModal: openFilter, closeModal: closeFilter } = useModalCommon({ initialValue: false })
const router = useRouter() const router = useRouter()
const {messages, removeMessage} = useMessage()
const toggleFilter = () => { const toggleFilter = () => {
if (visibleFilter) { if (visibleFilter) {
@@ -44,6 +46,7 @@ const LayoutContent: FC<Props> = ({ children }) => {
<Footer /> <Footer />
</div> </div>
<CartDrawer /> <CartDrawer />
<MessageCommon messages={messages} onRemove={removeMessage}/>
</> </>
) )

View File

@@ -1,15 +1,17 @@
import React from "react"; import React from 'react'
import s from './LoadingCommon.module.scss' import s from './LoadingCommon.module.scss'
const LoadingCommon = () => { interface Props {
description?: string
return (
<div className={s.wrapper}>
<div className={s.loadingCommon}>
</div>
<p className={s.text}>Loading...</p>
</div>
)
} }
export default LoadingCommon const LoadingCommon = ({ description = 'Loading...' }: Props) => {
return (
<div className={s.wrapper}>
<div className={s.loadingCommon}></div>
<p className={s.text}>{description}</p>
</div>
)
}
export default LoadingCommon

View File

@@ -24,7 +24,7 @@ const MenuNavigationProductList = ({categories,brands,featured,visible,onClose}:
setDataSort({...dataSort,...value}); setDataSort({...dataSort,...value});
} }
function filter(){ function filter(){
console.log(dataSort) // console.log(dataSort)
} }
return( return(
<> <>

View File

@@ -0,0 +1,7 @@
.messageCommon {
@apply fixed;
top: 2.4rem;
left: 50%;
z-index: 20000;
transform: translateX(-50%);
}

View File

@@ -0,0 +1,27 @@
import React, { memo, useEffect } from 'react'
import s from './MessageCommon.module.scss'
import MessageItem, { MessageItemProps } from './MessageItem/MessageItem'
interface Props {
messages: MessageItemProps[]
onRemove?: (id: number) => void
}
const MessageCommon = memo(({ messages, onRemove }: Props) => {
return (
<div className={s.messageCommon}>
{messages.reverse().map((item) => (
<MessageItem
key={item.id}
id={item.id}
type={item.type}
content={item.content}
onRemove={onRemove}
/>
))}
</div>
)
})
MessageCommon.displayName = 'MessageCommon'
export default MessageCommon

View File

@@ -0,0 +1,65 @@
@import "../../../../styles/utilities";
.messageItem {
@apply shadow-sm flex justify-center items-center cursor-default;
width: fit-content;
padding: 0.8rem 2.4rem;
margin: 0 auto .8rem;
border-radius: 0.8rem;
transition: all .5s;
animation: showMessage .5s;
width: max-content;
&.hide {
display: none;
}
.icon {
@apply flex justify-center items-center;
margin-right: 0.8rem;
svg {
width: 2rem;
height: 2rem;
}
}
&.info {
@apply bg-info-light;
color: var(--info-dark);
.icon svg path {
fill: var(--info);
}
}
&.success {
@apply bg-positive-light;
color: var(--positive-dark);
.icon svg path {
fill: var(--positive);
}
}
&.error {
@apply bg-negative-light;
color: var(--negative-dark);
.icon svg path {
fill: var(--negative);
}
}
&.warning {
@apply bg-warning-light;
color: var(--warning-dark);
.icon svg path {
fill: var(--warning);
}
}
}
@keyframes showMessage {
0% {
transform: translateY(-2rem);
opacity: .5;
}
100% {
transform: none;
opacity: 1;
}
}

View File

@@ -0,0 +1,71 @@
import classNames from 'classnames'
import React, { memo, useEffect, useMemo, useState } from 'react'
import { IconCheck, IconError, IconInfo } from 'src/components/icons'
import s from './MessageItem.module.scss'
export interface MessageItemProps {
id?: number
content?: React.ReactNode
type?: 'info' | 'success' | 'error' | 'warning'
timeout?: number
onRemove?: (id: number) => void
}
const MessageItem = memo(
({ id, content, type = 'success', timeout = 3000, onRemove }: MessageItemProps) => {
const [isHide, setIsHide] = useState<boolean>()
const [isMouseOver, setIsMouseOver] = useState(false)
const iconElement = useMemo(() => {
switch (type) {
case 'info':
return <IconInfo />
case 'success':
return <IconCheck />
case 'error':
return <IconError />
case 'warning':
return <IconError />
default:
return <IconInfo />
}
}, [type])
useEffect(() => {
setIsHide(false)
setTimeout(() => {
setIsHide(true)
}, timeout)
}, [timeout, isMouseOver])
useEffect(() => {
if (isHide && !isMouseOver && onRemove) {
onRemove(id || 0)
}
}, [isHide, isMouseOver, onRemove, id])
const onMouseOver = () => {
setIsMouseOver(true)
}
const onMouseLeave = () => {
setIsMouseOver(false)
}
return (
<div
className={classNames(s.messageItem, s[type], {
[s.hide]: isHide && !isMouseOver,
})}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
>
<span className={s.icon}>{iconElement}</span>
<span className={s.content}>{content}</span>
</div>
)
}
)
MessageItem.displayName = 'MessageItem'
export default MessageItem

View File

@@ -32,7 +32,7 @@ const ModalAuthenticate = ({ visible, mode, closeModal }: Props) => {
closeModal() closeModal()
router.push(ROUTE.ACCOUNT) router.push(ROUTE.ACCOUNT)
} }
}, [customer, visible]) }, [customer, visible, closeModal, router])
const onSwitch = () => { const onSwitch = () => {
setIsLogin(!isLogin) setIsLogin(!isLogin)

View File

@@ -1,7 +1,7 @@
@import '../../../../styles/utilities'; @import '../../../../styles/utilities';
.formAuthen { .formAuthen {
@apply bg-white w-full u-form; @apply bg-white w-full;
.inner { .inner {
@screen md { @screen md {
width: 60rem; width: 60rem;

View File

@@ -1,9 +1,13 @@
import { Form, Formik } from 'formik'
import Link from 'next/link' import Link from 'next/link'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef } from 'react'
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common' import { ButtonCommon, InputFiledInForm, InputPasswordFiledInForm } from 'src/components/common'
import { ROUTE } from 'src/utils/constanst.utils' import { useMessage } from 'src/components/contexts'
import { CustomInputCommon } from 'src/utils/type.utils'
import useLogin from 'src/components/hooks/useLogin' import useLogin from 'src/components/hooks/useLogin'
import { ROUTE } from 'src/utils/constanst.utils'
import { LANGUAGE } from 'src/utils/language.utils'
import { CustomInputCommon } from 'src/utils/type.utils'
import * as Yup from 'yup'
import s from '../FormAuthen.module.scss' import s from '../FormAuthen.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen' import SocialAuthen from '../SocialAuthen/SocialAuthen'
import styles from './FormLogin.module.scss' import styles from './FormLogin.module.scss'
@@ -13,15 +17,17 @@ interface Props {
onSwitch: () => void onSwitch: () => void
} }
const DisplayingErrorMessagesSchema = Yup.object().shape({
email: Yup.string().email('Your email was wrong').required('Required'),
password: Yup.string()
.max(30, 'Password is too long')
.required('Required'),
})
const FormLogin = ({ onSwitch, isHide }: Props) => { const FormLogin = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null) const emailRef = useRef<CustomInputCommon>(null)
const { loading, login, error } = useLogin() const { loading, login } = useLogin()
const [email, setEmail] = useState('') const { showMessageSuccess, showMessageError } = useMessage()
const [password, setPassword] = useState('')
const onLogin = () => {
login({ username: email, password })
}
useEffect(() => { useEffect(() => {
if (!isHide) { if (!isHide) {
@@ -29,42 +35,71 @@ const FormLogin = ({ onSwitch, isHide }: Props) => {
} }
}, [isHide]) }, [isHide])
useEffect(() => { const onLogin = (values: { email: string; password: string }) => {
if (error) { login({ username: values.email, password: values.password }, onLoginCallBack)
alert(error.message) }
const onLoginCallBack = (isSuccess: boolean, message?: string) => {
if (isSuccess) {
showMessageSuccess("Login successfully!", 4000)
} else {
showMessageError(message || LANGUAGE.MESSAGE.ERROR)
} }
}, [error]) }
return ( return (
<section className={s.formAuthen}> <section className={s.formAuthen}>
<div className={s.inner}> <div className={s.inner}>
<div className={s.body}> <div className={s.body}>
<Inputcommon <Formik
placeholder="Email Address" initialValues={{
value={email} password: '',
onChange={(val) => setEmail(val.toString())} email: '',
type="email" }}
ref={emailRef} validationSchema={DisplayingErrorMessagesSchema}
/> onSubmit={onLogin}
>
{({ errors, touched, isValid, submitForm }) => (
<Form className="u-form">
<div className="body">
<InputFiledInForm
name="email"
placeholder="Email Address"
ref={emailRef}
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()
: ''
}
onEnter={isValid ? submitForm : undefined}
/>
</div>
<div className={styles.bottom}>
<Link href={ROUTE.FORGOT_PASSWORD}>
<a href="" className={styles.forgotPassword}>
Forgot Password?
</a>
</Link>
<ButtonCommon HTMLType='submit' loading={loading} size="large">
Sign in
</ButtonCommon>
</div>
</Form>
)}
</Formik>
</div>
{/* <Inputcommon placeholder='Email Address' type='email' ref={emailRef}
isShowIconSuccess={true} isIconSuffix={true} /> */}
<InputPassword
placeholder="Password"
value={password}
onChange={(val) => setPassword(val.toString())}
/>
</div>
<div className={styles.bottom}>
<Link href={ROUTE.FORGOT_PASSWORD}>
<a href="" className={styles.forgotPassword}>
Forgot Password?
</a>
</Link>
<ButtonCommon onClick={onLogin} loading={loading} size="large">
Sign in
</ButtonCommon>
</div>
<SocialAuthen /> <SocialAuthen />
<div className={s.others}> <div className={s.others}>
<span>Don't have an account?</span> <span>Don't have an account?</span>

View File

@@ -1,49 +1,125 @@
import React, { useEffect, useRef } from 'react'
import { ButtonCommon, Inputcommon, InputPassword } from 'src/components/common'
import s from '../FormAuthen.module.scss'
import styles from './FormRegister.module.scss'
import SocialAuthen from '../SocialAuthen/SocialAuthen'
import classNames from 'classnames' import classNames from 'classnames'
import { Form, Formik } from 'formik'
import React, { useEffect, useRef } from 'react'
import {
ButtonCommon,
InputFiledInForm,
InputPasswordFiledInForm,
} from 'src/components/common'
import { useMessage } from 'src/components/contexts'
import { LANGUAGE } from 'src/utils/language.utils'
import { CustomInputCommon } from 'src/utils/type.utils' 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 { interface Props {
isHide: boolean, isHide: boolean
onSwitch: () => void 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 FormRegister = ({ onSwitch, isHide }: Props) => {
const emailRef = useRef<CustomInputCommon>(null) const emailRef = useRef<CustomInputCommon>(null)
const { loading, signup } = useSignup()
const { showMessageSuccess, showMessageError } = useMessage()
useEffect(() => { useEffect(() => {
if (!isHide) { if (!isHide) {
emailRef.current?.focus() emailRef.current?.focus()
} }
}, [isHide]) }, [isHide])
return ( const onSignup = (values: { email: string; password: string }) => {
<section className={classNames({ signup({ email: values.email, password: values.password }, onSignupCallBack)
[s.formAuthen]: true, }
[styles.formRegister]: true,
})}> const onSignupCallBack = (isSuccess: boolean, message?: string) => {
<div className={s.inner}> if (isSuccess) {
<div className={s.body}> showMessageSuccess("Create account successfully. Please verify your email to login.")
<Inputcommon placeholder='Email Address' type='email' ref={emailRef}/> } else {
<InputPassword placeholder='Password'/> showMessageError(message || LANGUAGE.MESSAGE.ERROR)
<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"
ref = {emailRef}
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>
<div className={styles.bottom}> <div className={styles.bottom}>
<ButtonCommon size='large'>Create Account</ButtonCommon> <ButtonCommon
HTMLType="submit"
size="large"
loading={loading}
>
Create Account
</ButtonCommon>
</div> </div>
<SocialAuthen /> </Form>
<div className={s.others}> )}
<span>Already an account?</span> </Formik>
<button onClick={onSwitch}>Sign In</button> </div>
</div> <SocialAuthen />
</div> <div className={s.others}>
</section> <span>Already an account?</span>
) <button onClick={onSwitch}>Sign In</button>
</div>
</div>
</section>
)
} }
export default FormRegister export default FormRegister

View File

@@ -47,4 +47,9 @@ export { default as StaticImage} from './StaticImage/StaticImage'
export { default as EmptyCommon} from './EmptyCommon/EmptyCommon' export { default as EmptyCommon} from './EmptyCommon/EmptyCommon'
export { default as CustomShapeSvg} from './CustomShapeSvg/CustomShapeSvg' export { default as CustomShapeSvg} from './CustomShapeSvg/CustomShapeSvg'
export { default as RecommendedRecipes} from './RecommendedRecipes/RecommendedRecipes' 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'
export { default as MessageCommon} from './MessageCommon/MessageCommon'

View File

@@ -0,0 +1,27 @@
import { createContext, useContext } from 'react'
import { MessageItemProps } from 'src/components/common/MessageCommon/MessageItem/MessageItem'
export type MessageContextType = {
messages: MessageItemProps[]
removeMessage: (id: number) => void
showMessageSuccess: (content: string, timeout?: number) => void
showMessageInfo: (content: string, timeout?: number) => void
showMessageError: (content: string, timeout?: number) => void
showMessageWarning: (content: string, timeout?: number) => void
}
export const DEFAULT_MESSAGE_CONTEXT: MessageContextType = {
messages: [],
removeMessage: () => {},
showMessageSuccess: () => {},
showMessageInfo: () => {},
showMessageError: () => {},
showMessageWarning: () => {},
}
export const MessageContext = createContext<MessageContextType>(
DEFAULT_MESSAGE_CONTEXT
)
export function useMessage() {
return useContext(MessageContext)
}

View File

@@ -0,0 +1,67 @@
import { ReactNode, useCallback, useState } from 'react'
import { MessageItemProps } from 'src/components/common/MessageCommon/MessageItem/MessageItem'
import { MessageContext } from './MessageContext'
type Props = {
children: ReactNode
}
export function MessageProvider({ children }: Props) {
const [currentId, setCurrentId] = useState<number>(0)
const [messages, setMessages] = useState<MessageItemProps[]>([])
const createNewMessage = (
content: string,
timeout?: number,
type?: 'info' | 'success' | 'error' | 'warning'
) => {
const item: MessageItemProps = {
id: currentId + 1,
content,
type,
timeout,
}
setCurrentId(currentId + 1)
setMessages([...messages, item])
}
const showMessageSuccess = (content: string, timeout?: number) => {
createNewMessage(content, timeout, 'success')
}
const showMessageInfo = (content: string, timeout?: number) => {
createNewMessage(content, timeout, 'info')
}
const showMessageError = (content: string, timeout?: number) => {
createNewMessage(content, timeout, 'error')
}
const showMessageWarning = (content: string, timeout?: number) => {
createNewMessage(content, timeout, 'warning')
}
const removeMessage = (id: number) => {
const newMessages = messages.filter((item) => item.id !== id)
setMessages(newMessages)
if (newMessages.length === 0) {
setCurrentId(0)
}
}
return (
<MessageContext.Provider
value={{
messages,
removeMessage,
showMessageSuccess,
showMessageInfo,
showMessageError,
showMessageWarning,
}}
>
{children}
</MessageContext.Provider>
)
}

View File

@@ -1,2 +1,5 @@
export * from './CartDrawer/CartDrawerContext' export * from './CartDrawer/CartDrawerContext'
export * from './CartDrawer/CartDrawerProvider' export * from './CartDrawer/CartDrawerProvider'
export * from './Message/MessageContext'
export * from './Message/MessageProvider'

View File

@@ -0,0 +1,3 @@
export { default as useLogout } from './useLogout'

View File

@@ -0,0 +1,34 @@
import { LogoutMutation } from '@framework/schema'
import { logoutMutation } from '@framework/utils/mutations/log-out-mutation'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import { LOCAL_STORAGE_KEY } from 'src/utils/constanst.utils'
import rawFetcher from 'src/utils/rawFetcher'
import useActiveCustomer from '../useActiveCustomer'
const useLogout = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useActiveCustomer()
const logout = () => {
setError(null)
setLoading(true)
rawFetcher<LogoutMutation>({
query: logoutMutation,
})
.then(({ data }) => {
if (!data.logout.success) {
throw CommonError.create('Logout fail')
}
localStorage.setItem(LOCAL_STORAGE_KEY.TOKEN, '')
mutate()
})
.catch(setError)
.finally(() => setLoading(false))
}
return { loading, logout, error }
}
export default useLogout

View File

@@ -1 +1,3 @@
export { default as useModalCommon } from './useModalCommon' export { default as useModalCommon } from './useModalCommon'
export { default as useVerifyCustomer } from './useVerifyCustomer'
export { default as useSignup } from './useSignup'

View File

@@ -4,6 +4,8 @@ import useActiveCustomer from './useActiveCustomer'
import { CommonError } from 'src/domains/interfaces/CommonError' import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher' import rawFetcher from 'src/utils/rawFetcher'
import { LoginMutation } from '@framework/schema' import { LoginMutation } from '@framework/schema'
import { LOCAL_STORAGE_KEY } from 'src/utils/constanst.utils'
import { errorMapping } from 'src/utils/errrorMapping'
const query = gql` const query = gql`
mutation login($username: String!, $password: String!) { mutation login($username: String!, $password: String!) {
@@ -30,7 +32,9 @@ const useLogin = () => {
const [error, setError] = useState<CommonError | null>(null) const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useActiveCustomer() const { mutate } = useActiveCustomer()
const login = (options: LoginInput) => { const login = (options: LoginInput,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null) setError(null)
setLoading(true) setLoading(true)
rawFetcher<LoginMutation>({ rawFetcher<LoginMutation>({
@@ -39,15 +43,19 @@ const useLogin = () => {
}) })
.then(({ data, headers }) => { .then(({ data, headers }) => {
if (data.login.__typename !== 'CurrentUser') { if (data.login.__typename !== 'CurrentUser') {
throw CommonError.create(data.login.message, data.login.errorCode) throw CommonError.create(errorMapping(data.login.message), data.login.errorCode)
} }
const authToken = headers.get('vendure-auth-token') const authToken = headers.get('vendure-auth-token')
if (authToken != null) { if (authToken != null) {
localStorage.setItem('token', authToken) localStorage.setItem(LOCAL_STORAGE_KEY.TOKEN, authToken)
return mutate() mutate()
} }
fCallBack(true)
})
.catch((error) => {
setError(error)
fCallBack(false, error.message)
}) })
.catch(setError)
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} }

View File

@@ -0,0 +1 @@
// here

View File

@@ -22,8 +22,8 @@ const query = gql`
interface SignupInput { interface SignupInput {
email: string email: string
firstName: string firstName?: string
lastName: string lastName?: string
password: string password: string
} }
@@ -32,7 +32,10 @@ const useSignup = () => {
const [error, setError] = useState<Error | null>(null) const [error, setError] = useState<Error | null>(null)
const { mutate } = useActiveCustomer() const { mutate } = useActiveCustomer()
const signup = ({ firstName, lastName, email, password }: SignupInput) => { const signup = (
{ firstName, lastName, email, password }: SignupInput,
fCallBack: (isSuccess: boolean, message?: string) => void
) => {
setError(null) setError(null)
setLoading(true) setLoading(true)
fetcher<SignupMutation>({ fetcher<SignupMutation>({
@@ -53,11 +56,15 @@ const useSignup = () => {
data.registerCustomerAccount.errorCode data.registerCustomerAccount.errorCode
) )
} }
console.log(data)
mutate() mutate()
fCallBack(true)
return data return data
}) })
.catch(setError) .catch((error) => {
setError(error)
fCallBack(false, error.message)
})
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} }

View File

@@ -0,0 +1,51 @@
import { VerifyCustomerAccountMutation } from '@framework/schema'
import { useState } from 'react'
import { CommonError } from 'src/domains/interfaces/CommonError'
import rawFetcher from 'src/utils/rawFetcher'
import { VERIFY_CUSTOMER_ACCOUNT } from '../../graphql/mutation'
import useActiveCustomer from './useActiveCustomer'
interface VerifyInput {
token: string
password?: string
}
const useVerifyCustomer = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<CommonError | null>(null)
const { mutate } = useActiveCustomer()
const verify = (
options: VerifyInput,
fCallBack?: (isSuccess: boolean) => void
) => {
setError(null)
setLoading(true)
rawFetcher<VerifyCustomerAccountMutation>({
query: VERIFY_CUSTOMER_ACCOUNT,
variables: options,
})
.then(({ data }) => {
if (data.verifyCustomerAccount.__typename !== 'CurrentUser') {
throw CommonError.create(
data.verifyCustomerAccount.message,
data.verifyCustomerAccount.errorCode
)
}
fCallBack && fCallBack(true)
mutate()
return data
})
.catch((err) => {
setError(err)
fCallBack && fCallBack(false)
})
.finally(() => {
setLoading(false)
})
}
return { loading, verify, error }
}
export default useVerifyCustomer

View File

@@ -87,7 +87,7 @@ const AccountPage = ({ defaultActiveContent="orders" } : AccountPageProps) => {
const query = router.query[QUERY_KEY.TAB] as string const query = router.query[QUERY_KEY.TAB] as string
const index = getTabIndex(query) const index = getTabIndex(query)
setActiveTab(index) setActiveTab(index)
}, [router.query[QUERY_KEY.TAB]]) }, [router.query])
function showModal() { function showModal() {
setModalVisible(true); setModalVisible(true);

View File

@@ -12,8 +12,6 @@ interface EditInfoModalProps {
const EditInfoModal = ({ accountInfo, visible = false, closeModal }: EditInfoModalProps) => { const EditInfoModal = ({ accountInfo, visible = false, closeModal }: EditInfoModalProps) => {
function saveInfo() { function saveInfo() {
console.log("saved !!!");
closeModal(); closeModal();
} }

View File

@@ -16,7 +16,6 @@ const FormSubscribe = () => {
e.preventDefault && e.preventDefault() e.preventDefault && e.preventDefault()
value = inputElementRef.current?.getValue()?.toString() || '' value = inputElementRef.current?.getValue()?.toString() || ''
} }
console.log("email here: ", value)
} }
return ( return (

View File

@@ -0,0 +1,27 @@
@import "../../../../styles/utilities";
.verifyCustomerAccount {
@apply spacing-horizontal;
min-height: 25rem;
margin: 5rem auto 10rem;
display: flex;
justify-content: center;
.result {
@apply flex flex-col justify-center items-center;
.message {
margin-bottom: 1.6rem;
}
.bottom {
@apply flex justify-center items-center flex-col;
a {
margin-right: 1.6rem;
margin-bottom: 1.6rem;
width: 100%;
}
button {
@apply w-full;
}
}
}
}

View File

@@ -0,0 +1,70 @@
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { ButtonCommon } from 'src/components/common'
import LoadingCommon from 'src/components/common/LoadingCommon/LoadingCommon'
import { useModalCommon, useVerifyCustomer } from 'src/components/hooks'
import { ROUTE } from 'src/utils/constanst.utils'
import s from './VerifyCustomerAccount.module.scss'
import Link from 'next/link'
import { LANGUAGE } from 'src/utils/language.utils'
import ModalAuthenticate from 'src/components/common/ModalAuthenticate/ModalAuthenticate'
export default function VerifyCustomerAccount() {
const router = useRouter()
const [isVerified, setIsVerified] = useState<boolean>(false)
const { error, loading, verify } = useVerifyCustomer()
const {
visible: visibleModalAuthen,
closeModal: closeModalAuthen,
openModal: openModalAuthen,
} = useModalCommon({ initialValue: false })
useEffect(() => {
const token = router.query.token
if (token && !isVerified) {
setIsVerified(true)
verify({ token: token.toString() })
}
}, [router, verify, isVerified])
return (
<div className={s.verifyCustomerAccount}>
{loading || !isVerified ? (
<div>
<LoadingCommon description="Verifing your account ...." />
</div>
) : error ? (
<div className={s.result}>
<div className={s.message}>Error: {error?.message}</div>
<Link href={ROUTE.HOME}>
<a href="">
<ButtonCommon>Back to home</ButtonCommon>
</a>
</Link>
</div>
) : (
<div className={s.result}>
<div className={s.message}>
Congratulation! Verified account successfully
</div>
<div className={s.bottom}>
<Link href={ROUTE.HOME}>
<a href="">
<ButtonCommon type="light">Back to home</ButtonCommon>
</a>
</Link>
<ButtonCommon onClick={openModalAuthen}>
{LANGUAGE.BUTTON_LABEL.SIGNIN}
</ButtonCommon>
</div>
</div>
)}
<ModalAuthenticate
visible={visibleModalAuthen}
closeModal={closeModalAuthen}
/>
</div>
)
}

View File

@@ -0,0 +1,2 @@
export { default as VerifyCustomerAccount } from './VerifyCustomerAccount/VerifyCustomerAccount'

View File

@@ -0,0 +1 @@
export * from './user.mutation'

View File

@@ -0,0 +1,20 @@
import { gql } from 'graphql-request'
export const VERIFY_CUSTOMER_ACCOUNT = gql`
mutation verifyCustomerAccount($token: String!, $password: String) {
verifyCustomerAccount( token: $token, password: $password) {
__typename
...on CurrentUser {
id
identifier
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1 @@
// export * from './user.mutation'

View File

@@ -0,0 +1 @@
// query here

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 "~tailwindcss/utilities";
@import './utilities'; @import './utilities';
@import './form';
@import './pages' @import './pages'

View File

@@ -38,6 +38,10 @@ export const ACCOUNT_TAB = {
FAVOURITE: 'wishlist', FAVOURITE: 'wishlist',
} }
export const LOCAL_STORAGE_KEY = {
TOKEN: 'token'
}
export const QUERY_KEY = { export const QUERY_KEY = {
TAB: 'tab', TAB: 'tab',
CATEGORY: 'category', CATEGORY: 'category',

View File

@@ -0,0 +1,14 @@
import { LANGUAGE } from "./language.utils";
export function errorMapping(message?: string) {
if (!message) {
return LANGUAGE.MESSAGE.ERROR
}
switch (message) {
case 'The provided credentials are invalid':
return 'The email address or password is incorrect!'
default:
return LANGUAGE.MESSAGE.ERROR
}
}

View File

@@ -1,5 +1,6 @@
import { request } from 'graphql-request' import { request } from 'graphql-request'
import { RequestDocument, Variables } from 'graphql-request/dist/types' import { RequestDocument, Variables } from 'graphql-request/dist/types'
import { LOCAL_STORAGE_KEY } from './constanst.utils'
interface QueryOptions { interface QueryOptions {
query: RequestDocument query: RequestDocument
@@ -10,11 +11,7 @@ interface QueryOptions {
const fetcher = async <T>(options: QueryOptions): Promise<T> => { const fetcher = async <T>(options: QueryOptions): Promise<T> => {
const { query, variables } = options const { query, variables } = options
console.log('query') const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
console.log(options)
const token = localStorage.getItem('token')
console.log('token')
console.log(token)
const res = await request<T>( const res = await request<T>(
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string,
query, query,

View File

@@ -9,5 +9,8 @@ export const LANGUAGE = {
}, },
PLACE_HOLDER: { PLACE_HOLDER: {
SEARCH: 'Search', SEARCH: 'Search',
},
MESSAGE: {
ERROR: 'Something went wrong! Please try again!'
} }
} }

View File

@@ -1,5 +1,6 @@
import { rawRequest } from 'graphql-request' import { rawRequest } from 'graphql-request'
import { RequestDocument, Variables } from 'graphql-request/dist/types' import { RequestDocument, Variables } from 'graphql-request/dist/types'
import { LOCAL_STORAGE_KEY } from './constanst.utils'
interface QueryOptions { interface QueryOptions {
query: RequestDocument query: RequestDocument
@@ -14,10 +15,12 @@ const rawFetcher = <T>({
onLoad = () => true, onLoad = () => true,
}: QueryOptions): Promise<{ data: T; headers: any }> => { }: QueryOptions): Promise<{ data: T; headers: any }> => {
onLoad(true) onLoad(true)
const token = localStorage.getItem(LOCAL_STORAGE_KEY.TOKEN)
return rawRequest<T>( return rawRequest<T>(
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string, process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL as string,
query as string, query as string,
variables variables,
token ? { Authorization: 'Bearer ' + token } : {}
) )
.then(({ data, headers }) => { .then(({ data, headers }) => {
return { data, headers } return { data, headers }

View File

@@ -445,7 +445,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.2" regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.0": "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.0":
version "7.15.4" version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
@@ -1213,6 +1213,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.173.tgz#9d3b674c67a26cf673756f6aca7b429f237f91ed" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.173.tgz#9d3b674c67a26cf673756f6aca7b429f237f91ed"
integrity sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg== integrity sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==
"@types/lodash@^4.14.165":
version "4.14.175"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
"@types/lru-cache@4.1.1": "@types/lru-cache@4.1.1":
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40"
@@ -2572,6 +2577,11 @@ deepmerge@4.2.2, deepmerge@^4.0.0, deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
defer-to-connect@^1.0.1: defer-to-connect@^1.0.1:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
@@ -3279,6 +3289,19 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
formik@^2.2.9:
version "2.2.9"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
dependencies:
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 "^1.10.0"
fraction.js@^4.1.1: fraction.js@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff"
@@ -3595,6 +3618,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1" minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@@ -4415,6 +4445,11 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.camelcase@^4.3.0: lodash.camelcase@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@@ -4752,6 +4787,11 @@ mute-stream@0.0.8:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.23: nanoid@^3.1.23:
version "3.1.25" version "3.1.25"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
@@ -5795,6 +5835,11 @@ prop-types@^15.7.2:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
property-expr@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
public-encrypt@^4.0.0: public-encrypt@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -5921,6 +5966,11 @@ react-dom@^17.0.2:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.2" scheduler "^0.20.2"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-fast-compare@^3.0.1: react-fast-compare@^3.0.1:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
@@ -5936,7 +5986,7 @@ react-is@17.0.2:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.8.1: react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6878,6 +6928,11 @@ timers-browserify@2.0.12, timers-browserify@^2.0.4:
dependencies: dependencies:
setimmediate "^1.0.4" setimmediate "^1.0.4"
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
title-case@^3.0.3: title-case@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982" resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982"
@@ -6926,6 +6981,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
totalist@^1.0.0: totalist@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
@@ -6970,7 +7030,7 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
minimist "^1.2.0" minimist "^1.2.0"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -7419,3 +7479,16 @@ yocto-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.32.9:
version "0.32.9"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872"
integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==
dependencies:
"@babel/runtime" "^7.10.5"
"@types/lodash" "^4.14.165"
lodash "^4.17.20"
lodash-es "^4.17.15"
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"