diff --git a/components/auth/LoginView.tsx b/components/auth/LoginView.tsx index fb7f99ada..e597c9d26 100644 --- a/components/auth/LoginView.tsx +++ b/components/auth/LoginView.tsx @@ -1,98 +1,90 @@ -import { FC, useEffect } from 'react' -import { Logo, Modal, Button } from '@components/ui' -import useSignup from '@lib/bigcommerce/use-signup' +import { FC, useEffect, useState } from 'react' +import { Logo, Modal, Button, Input } from '@components/ui' import useLogin from '@lib/bigcommerce/use-login' -import useLogout from '@lib/bigcommerce/use-logout' -import useCustomer from '@lib/bigcommerce/use-customer' import { useUI } from '@components/ui/context' +import { validate } from 'email-validator' -interface Props { - open: boolean -} +interface Props {} + +const LoginView: FC = () => { + // Form State + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [dirty, setDirty] = useState(false) + const [disabled, setDisabled] = useState(false) + const { setModalView, closeModal } = useUI() -const LoginView: FC = ({ open }) => { - const signup = useSignup() const login = useLogin() - const logout = useLogout() - // // Data about the currently logged in customer, it will update - // // automatically after a signup/login/logout - // const { data } = useCustomer() - // TODO: use this method. It can take more than 5 seconds to do a signup - const handleSignup = async () => { - // TODO: validate the password and email before calling the signup - // Passwords must be at least 7 characters and contain both alphabetic - // and numeric characters. - try { - await signup({ - // This account already exists, so it will throw the "duplicated_email" error - email: 'luis@vercel.com', - firstName: 'Luis', - lastName: 'Alvarez', - password: 'luis123', - }) - } catch (error) { - if (error.code === 'duplicated_email') { - // TODO: handle duplicated email - } - // Show a generic error saying that something bad happened, try again later - } - } const handleLogin = async () => { - // TODO: validate the password and email before calling the signup - // Passwords must be at least 7 characters and contain both alphabetic - // and numeric characters. + if (!dirty && !disabled) { + setDirty(true) + handleValidation() + } + try { + setLoading(true) + setMessage('') await login({ - email: 'luis@vercel.com', - // This is an invalid password so it will throw the "invalid_credentials" error - password: 'luis1234', // will work with `luis123` + email, + password, }) - } catch (error) { - if (error.code === 'invalid_credentials') { - // The email and password didn't match an existing account - } - // Show a generic error saying that something bad happened, try again later + setLoading(false) + closeModal() + } catch ({ errors }) { + setMessage(errors[0].message) + setLoading(false) + } + } + + const handleValidation = () => { + // Test for Alphanumeric password + const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password) + + // Unable to send form unless fields are valid. + if (dirty) { + setDisabled(!validate(email) || password.length < 7 || !validPassword) } } - const { openModal, closeModal } = useUI() useEffect(() => { - open ? openModal() : closeModal() - }, [open]) + handleValidation() + }, [email, password, dirty]) return ( - -
-
- -
-
-
- -
-
- -
- - - Don't have an account? - {` `} - - Sign Up - - -
+
+
+
- +
+ {message && ( +
{message}
+ )} + + + + + + Don't have an account? + {` `} + setModalView('SIGNUP_VIEW')} + > + Sign Up + + +
+
) } diff --git a/components/auth/SignUpView.tsx b/components/auth/SignUpView.tsx new file mode 100644 index 000000000..f46184bd0 --- /dev/null +++ b/components/auth/SignUpView.tsx @@ -0,0 +1,96 @@ +import { FC, useEffect, useState } from 'react' +import { Logo, Button, Input } from '@components/ui' +import useSignup from '@lib/bigcommerce/use-signup' +import { useUI } from '@components/ui/context' +import { validate } from 'email-validator' + +interface Props {} + +const SignUpView: FC = () => { + // Form State + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [dirty, setDirty] = useState(false) + const [disabled, setDisabled] = useState(false) + + const signup = useSignup() + const { setModalView, closeModal } = useUI() + + const handleSignup = async () => { + if (!dirty && !disabled) { + setDirty(true) + handleValidation() + } + + try { + setLoading(true) + setMessage('') + await signup({ + email, + firstName, + lastName, + password, + }) + setLoading(false) + closeModal() + } catch ({ errors }) { + setMessage(errors[0].message) + setLoading(false) + } + } + + const handleValidation = () => { + // Test for Alphanumeric password + const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password) + + // Unable to send form unless fields are valid. + if (dirty) { + setDisabled(!validate(email) || password.length < 7 || !validPassword) + } + } + + useEffect(() => { + handleValidation() + }, [email, password, dirty]) + + return ( +
+
+ +
+
+ {message && ( +
{message}
+ )} + + + + + + + Do you have an account? + {` `} + setModalView('LOGIN_VIEW')} + > + Log In + + +
+
+ ) +} + +export default SignUpView diff --git a/components/auth/index.ts b/components/auth/index.ts index fa4dd296f..e0f739bc5 100644 --- a/components/auth/index.ts +++ b/components/auth/index.ts @@ -1 +1,2 @@ export { default as LoginView } from './LoginView' +export { default as SignUpView } from './SignUpView' diff --git a/components/core/Layout/Layout.tsx b/components/core/Layout/Layout.tsx index a1d96eb2c..f76eed877 100644 --- a/components/core/Layout/Layout.tsx +++ b/components/core/Layout/Layout.tsx @@ -1,14 +1,14 @@ -import { FC, useEffect, useState } from 'react' import cn from 'classnames' -import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages' -import { CommerceProvider } from '@lib/bigcommerce' -import { Navbar, Featurebar, Footer } from '@components/core' -import { Container, Sidebar } from '@components/ui' -import Button from '@components/ui/Button' -import { CartSidebarView } from '@components/cart' -import { useUI } from '@components/ui/context' import s from './Layout.module.css' +import React, { FC, useEffect, useState } from 'react' +import { CartSidebarView } from '@components/cart' +import { Container, Sidebar, Button, Modal } from '@components/ui' +import { Navbar, Featurebar, Footer } from '@components/core' +import { LoginView, SignUpView } from '@components/auth' +import { useUI } from '@components/ui/context' import { usePreventScroll } from '@react-aria/overlays' +import { CommerceProvider } from '@lib/bigcommerce' +import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages' interface Props { pageProps: { pages?: Page[] @@ -16,7 +16,13 @@ interface Props { } const Layout: FC = ({ children, pageProps }) => { - const { displaySidebar, displayDropdown, closeSidebar } = useUI() + const { + displaySidebar, + displayModal, + closeSidebar, + closeModal, + modalView, + } = useUI() const [acceptedCookies, setAcceptedCookies] = useState(false) const [hasScrolled, setHasScrolled] = useState(false) @@ -37,7 +43,7 @@ const Layout: FC = ({ children, pageProps }) => { }, []) usePreventScroll({ - isDisabled: !displaySidebar, + isDisabled: !(displaySidebar || displayModal), }) return ( @@ -55,11 +61,13 @@ const Layout: FC = ({ children, pageProps }) => {
{children}
- - + + {modalView === 'LOGIN_VIEW' && } + {modalView === 'SIGNUP_VIEW' && } + = ({ open = false }) => { const { theme, setTheme } = useTheme() - + const logout = useLogout() return ( = ({ open = false }) => { - Logout + logout()} + > + Logout + diff --git a/components/core/UserNav/UserNav.tsx b/components/core/UserNav/UserNav.tsx index f129c4a2e..d7e2adf28 100644 --- a/components/core/UserNav/UserNav.tsx +++ b/components/core/UserNav/UserNav.tsx @@ -22,7 +22,7 @@ const UserNav: FC = ({ className, children, ...props }) => { const { data } = useCart() const { data: customer } = useCustomer() - const { openSidebar, closeSidebar, displaySidebar } = useUI() + const { openSidebar, closeSidebar, displaySidebar, openModal } = useUI() const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0) return (
diff --git a/components/ui/Button/Button.module.css b/components/ui/Button/Button.module.css index 266e9e78d..ffdbb7cae 100644 --- a/components/ui/Button/Button.module.css +++ b/components/ui/Button/Button.module.css @@ -24,3 +24,12 @@ .slim { @apply py-2 transform-none normal-case; } + +.disabled, +.disabled:hover { + @apply text-accents-4 border-accents-2 bg-accents-1 cursor-not-allowed; + filter: grayscale(1); + -webkit-transform: translateZ(0); + -webkit-perspective: 1000; + -webkit-backface-visibility: hidden; +} diff --git a/components/ui/Button/Button.tsx b/components/ui/Button/Button.tsx index 22e5b3605..6a68d19e0 100644 --- a/components/ui/Button/Button.tsx +++ b/components/ui/Button/Button.tsx @@ -19,6 +19,7 @@ export interface ButtonProps extends ButtonHTMLAttributes { Component?: string | JSXElementConstructor width?: string | number loading?: boolean + disabled?: boolean } const Button: React.FC = forwardRef((props, buttonRef) => { @@ -28,10 +29,10 @@ const Button: React.FC = forwardRef((props, buttonRef) => { children, active, onClick, - disabled, width, Component = 'button', loading = false, + disabled = false, style = {}, ...rest } = props @@ -52,6 +53,7 @@ const Button: React.FC = forwardRef((props, buttonRef) => { { [s.slim]: variant === 'slim', [s.loading]: loading, + [s.disabled]: disabled, }, className ) @@ -64,6 +66,7 @@ const Button: React.FC = forwardRef((props, buttonRef) => { {...buttonProps} data-active={isPressed ? '' : undefined} className={rootClassName} + disabled={disabled} style={{ width, ...style, diff --git a/components/ui/Input/Input.module.css b/components/ui/Input/Input.module.css new file mode 100644 index 000000000..ccaf833db --- /dev/null +++ b/components/ui/Input/Input.module.css @@ -0,0 +1,5 @@ +.root { + @apply focus:outline-none bg-primary focus:shadow-outline-gray py-2 + px-6 w-full appearance-none transition duration-150 ease-in-out + placeholder-accents-5 pr-10 border border-accents-3 text-accents-6; +} diff --git a/components/ui/Input/Input.tsx b/components/ui/Input/Input.tsx new file mode 100644 index 000000000..86ae34fc9 --- /dev/null +++ b/components/ui/Input/Input.tsx @@ -0,0 +1,25 @@ +import cn from 'classnames' +import s from './Input.module.css' +import React, { InputHTMLAttributes } from 'react' + +export interface Props extends InputHTMLAttributes { + className?: string + onChange?: (...args: any[]) => any +} + +const Input: React.FC = (props) => { + const { className, children, onChange, ...rest } = props + + const rootClassName = cn(s.root, {}, className) + + const handleOnChange = (e: any) => { + if (onChange) { + onChange(e.target.value) + } + return null + } + + return +} + +export default Input diff --git a/components/ui/Input/index.ts b/components/ui/Input/index.ts new file mode 100644 index 000000000..aa97178e5 --- /dev/null +++ b/components/ui/Input/index.ts @@ -0,0 +1 @@ +export { default } from './Input' diff --git a/components/ui/Modal/Modal.tsx b/components/ui/Modal/Modal.tsx index bafc0ae6f..c19a21b10 100644 --- a/components/ui/Modal/Modal.tsx +++ b/components/ui/Modal/Modal.tsx @@ -8,7 +8,7 @@ import { useOverlay, useModal, OverlayContainer } from '@react-aria/overlays' interface Props { className?: string children?: any - open?: boolean + open: boolean onClose?: () => void } @@ -22,14 +22,22 @@ const Modal: FC = ({ const rootClassName = cn(s.root, className) let ref = useRef() as React.MutableRefObject let { modalProps } = useModal() - let { overlayProps } = useOverlay(props, ref) - let { dialogProps } = useDialog(props, ref) + let { dialogProps } = useDialog({}, ref) + let { overlayProps } = useOverlay( + { + isOpen: open, + isDismissable: true, + onClose: onClose, + ...props, + }, + ref + ) return ( -
- + +
= ({ leaveTo="opacity-0" >
{children}
- -
+
+
) diff --git a/components/ui/context.tsx b/components/ui/context.tsx index bacbca02f..44343fcb4 100644 --- a/components/ui/context.tsx +++ b/components/ui/context.tsx @@ -6,12 +6,14 @@ export interface State { displaySidebar: boolean displayDropdown: boolean displayModal: boolean + modalView: string } const initialState = { displaySidebar: false, displayDropdown: false, displayModal: false, + modalView: 'LOGIN_VIEW', } type Action = @@ -33,6 +35,16 @@ type Action = | { type: 'CLOSE_MODAL' } + | { + type: 'SET_MODAL_VIEW' + view: 'LOGIN_VIEW' + } + | { + type: 'SET_MODAL_VIEW' + view: 'SIGNUP_VIEW' + } + +type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' export const UIContext = React.createContext(initialState) @@ -76,6 +88,12 @@ function uiReducer(state: State, action: Action) { displayModal: false, } } + case 'SET_MODAL_VIEW': { + return { + ...state, + modalView: action.view, + } + } } } @@ -91,6 +109,9 @@ export const UIProvider: FC = (props) => { const openModal = () => dispatch({ type: 'OPEN_MODAL' }) const closeModal = () => dispatch({ type: 'CLOSE_MODAL' }) + const setModalView = (view: MODAL_VIEWS) => + dispatch({ type: 'SET_MODAL_VIEW', view }) + const value = { ...state, openSidebar, @@ -99,10 +120,9 @@ export const UIProvider: FC = (props) => { closeDropdown, openModal, closeModal, + setModalView, } - console.log('state', state) - return } diff --git a/components/ui/index.ts b/components/ui/index.ts index a53f0b513..581c12d53 100644 --- a/components/ui/index.ts +++ b/components/ui/index.ts @@ -9,3 +9,4 @@ export { default as LoadingDots } from './LoadingDots' export { default as Skeleton } from './Skeleton' export { default as Modal } from './Modal' export { default as Text } from './Text' +export { default as Input } from './Input' diff --git a/package.json b/package.json index a04aeb610..333c897ae 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "bowser": "^2.11.0", "classnames": "^2.2.6", "cookie": "^0.4.1", + "email-validator": "^2.0.4", "intersection-observer": "^0.11.0", "js-cookie": "^2.2.1", "keen-slider": "^5.2.4", diff --git a/yarn.lock b/yarn.lock index 9e2a732c0..fb90e1970 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4030,6 +4030,11 @@ elliptic@^6.5.3: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +email-validator@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" + integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"