feat: login & home
This commit is contained in:
parent
c1b7cdcbfa
commit
06358334c4
14
src/App.tsx
14
src/App.tsx
@ -1,13 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
import { BreakpointProvider } from './components/Breakpoint'
|
import { BreakpointProvider } from './components/Breakpoint'
|
||||||
import { Header } from './components/Layout/Header'
|
import { Layout } from './components/Layout'
|
||||||
import { Footer } from './components/Layout/Footer'
|
import { Home } from './pages/activation/home'
|
||||||
|
import { Login } from './pages/activation/login'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BreakpointProvider>
|
<BreakpointProvider>
|
||||||
<Header showActiveKit showMenu />
|
<Routes>
|
||||||
<Footer />
|
<Route path="/" element={<Layout />}>
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
</BreakpointProvider>
|
</BreakpointProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ export const useBreakpoint = (props: BreakpointProps): boolean => {
|
|||||||
return size.width === breakpointList[key as keyof typeof breakpointList]
|
return size.width === breakpointList[key as keyof typeof breakpointList]
|
||||||
}
|
}
|
||||||
if (key === 'width') {
|
if (key === 'width') {
|
||||||
return matchWidth(3000, props.width?.min, props.width?.max)
|
return matchWidth(size.width, props.width?.min, props.width?.max)
|
||||||
}
|
}
|
||||||
if (key === 'landscape' || key === 'portrait') {
|
if (key === 'landscape' || key === 'portrait') {
|
||||||
return key === orientation
|
return key === orientation
|
||||||
|
20
src/components/FormControl/ErrorMessage.tsx
Normal file
20
src/components/FormControl/ErrorMessage.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { FC, PropsWithChildren } from 'react'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const StyledIcon = styled(InfoCircleOutlined)`
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16px;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
font-size: 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
line-height: 10px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const ErrorMessage: FC<PropsWithChildren<{ message?: string }>> = ({ message, children }) => {
|
||||||
|
return <><StyledIcon />{children || message}</>
|
||||||
|
}
|
31
src/components/FormControl/LabelWithTooltip.tsx
Normal file
31
src/components/FormControl/LabelWithTooltip.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { FC, PropsWithChildren } from 'react'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { Tooltip } from '../Tooltip'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const StyledLabel = styled.span`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledIcon = styled(InfoCircleOutlined)`
|
||||||
|
position: absolute;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #000022;
|
||||||
|
right: 0;
|
||||||
|
`
|
||||||
|
export const LabelWithTooltip: FC<PropsWithChildren<{ title: string; primary?: boolean }>> = ({
|
||||||
|
primary,
|
||||||
|
children,
|
||||||
|
title
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledLabel>
|
||||||
|
{children}
|
||||||
|
<Tooltip closeable title={title} primary={primary} placement="left">
|
||||||
|
<StyledIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</StyledLabel>
|
||||||
|
)
|
||||||
|
}
|
@ -5,3 +5,5 @@ export * from './Select'
|
|||||||
export * from './Chekbox'
|
export * from './Chekbox'
|
||||||
export * from './Radio'
|
export * from './Radio'
|
||||||
export * from './RadioGroup'
|
export * from './RadioGroup'
|
||||||
|
export * from './ErrorMessage'
|
||||||
|
export * from './LabelWithTooltip'
|
||||||
|
@ -119,12 +119,12 @@ const TinyFooter = styled.footer`
|
|||||||
const StyledFooter: FC<PropsWithChildren> = ({ children }) => {
|
const StyledFooter: FC<PropsWithChildren> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breakpoint s down>
|
<Breakpoint width={{ max: 520 }}>
|
||||||
<TinyFooter>
|
<TinyFooter>
|
||||||
{children}
|
{children}
|
||||||
</TinyFooter>
|
</TinyFooter>
|
||||||
</Breakpoint>
|
</Breakpoint>
|
||||||
<Breakpoint s up>
|
<Breakpoint width={{ min: 520 }}>
|
||||||
<LargeFooter>
|
<LargeFooter>
|
||||||
{children}
|
{children}
|
||||||
</LargeFooter>
|
</LargeFooter>
|
||||||
@ -133,9 +133,9 @@ const StyledFooter: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
export const Footer: FC = () => {
|
export const Footer: FC<{ className?: string }> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StyledFooter>
|
<StyledFooter {...props}>
|
||||||
<Links>
|
<Links>
|
||||||
{
|
{
|
||||||
FooterLinks.map((link, index) => (
|
FooterLinks.map((link, index) => (
|
||||||
|
@ -20,7 +20,6 @@ const StyledHeader = styled.header`
|
|||||||
border-bottom: 2px solid #000022;
|
border-bottom: 2px solid #000022;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledLogo = styled(Logo)`
|
const StyledLogo = styled(Logo)`
|
||||||
width: 171px;
|
width: 171px;
|
||||||
height: 41px;
|
height: 41px;
|
||||||
@ -78,11 +77,12 @@ const StyledButton = styled(Button)`
|
|||||||
height: 58px;
|
height: 58px;
|
||||||
|
|
||||||
${props => props.theme.breakpoints.down(600)} {
|
${props => props.theme.breakpoints.down(600)} {
|
||||||
width: 160px;
|
width: 130px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface HeaderProps {
|
export interface HeaderProps {
|
||||||
|
className?: string
|
||||||
showMenu?: boolean
|
showMenu?: boolean
|
||||||
onClickMenu?: () => void
|
onClickMenu?: () => void
|
||||||
showActiveKit?: boolean;
|
showActiveKit?: boolean;
|
||||||
@ -101,10 +101,11 @@ export const Header: FC<HeaderProps> = ({
|
|||||||
showMenu,
|
showMenu,
|
||||||
onClickMenu,
|
onClickMenu,
|
||||||
showActiveKit,
|
showActiveKit,
|
||||||
activeKitHref
|
activeKitHref,
|
||||||
|
className
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledHeader>
|
<StyledHeader className={className}>
|
||||||
{showMenu &&
|
{showMenu &&
|
||||||
<Breakpoint s down>
|
<Breakpoint s down>
|
||||||
<StyledMenuIcon component={Menu} onClick={onClickMenu} />
|
<StyledMenuIcon component={Menu} onClick={onClickMenu} />
|
||||||
@ -115,7 +116,8 @@ export const Header: FC<HeaderProps> = ({
|
|||||||
<Breakpoint s up>
|
<Breakpoint s up>
|
||||||
<StyledButton href={activeKitHref}>Activate Kit</StyledButton>
|
<StyledButton href={activeKitHref}>Activate Kit</StyledButton>
|
||||||
</Breakpoint>}
|
</Breakpoint>}
|
||||||
<StyledCartWrapper target="_blank" rel="noopener noreferrer" href="https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2">
|
<StyledCartWrapper target="_blank" rel="noopener noreferrer"
|
||||||
|
href="https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2">
|
||||||
<Icon component={Cart} />
|
<Icon component={Cart} />
|
||||||
<StyledText>SHOP</StyledText>
|
<StyledText>SHOP</StyledText>
|
||||||
</StyledCartWrapper>
|
</StyledCartWrapper>
|
||||||
|
36
src/components/Layout/Layout.tsx
Normal file
36
src/components/Layout/Layout.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { FC, PropsWithChildren } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Header, HeaderProps } from './Header'
|
||||||
|
import { Footer } from './Footer'
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
|
const StyledSection = styled.section`
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledHeader = styled(Header)`
|
||||||
|
flex: 0 0 auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledFooter = styled(Footer)`
|
||||||
|
flex: 0 0 auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledMain = styled.main`
|
||||||
|
flex: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Layout: FC<PropsWithChildren<Omit<HeaderProps, 'className'>>> = ({ children, ...headerProps }) => {
|
||||||
|
return (
|
||||||
|
<StyledSection>
|
||||||
|
<StyledHeader {...headerProps} />
|
||||||
|
<StyledMain>
|
||||||
|
<Outlet />
|
||||||
|
{children}
|
||||||
|
</StyledMain>
|
||||||
|
<StyledFooter />
|
||||||
|
</StyledSection>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Layout/index.ts
Normal file
1
src/components/Layout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Layout'
|
@ -1,15 +1,19 @@
|
|||||||
import React, { FC, useCallback } from 'react'
|
import React, { FC, ReactNode, useCallback } from 'react'
|
||||||
import { Tooltip as AntdTooltip, TooltipProps as AntdTooltipProps } from 'antd'
|
import { Tooltip as AntdTooltip, TooltipProps as AntdTooltipProps } from 'antd'
|
||||||
import { ReactComponent as CloseIcon } from '../../icons/close.svg'
|
import { ReactComponent as CloseIcon } from '../../icons/close.svg'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
export type TooltipProps = AntdTooltipProps & { closeable?: boolean; primary?: boolean }
|
export type TooltipProps = Omit<AntdTooltipProps, 'title'> & {
|
||||||
|
closeable?: boolean; primary?: boolean;
|
||||||
|
title: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
const Close = styled.span`
|
const Close = styled.span`
|
||||||
line-height: 17px;
|
line-height: 25px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
> svg {
|
> svg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
@ -27,7 +31,8 @@ export const Tooltip: FC<TooltipProps> = ({ closeable, primary, title, overlayCl
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
overlayClassName={`health-tooltip${primary ? '-highlighted' : ''} ${overlayClassName || ''}`}
|
overlayClassName={`health-tooltip${primary ? '-highlighted' : ''} ${overlayClassName || ''}`}
|
||||||
{...props}
|
{...props}
|
||||||
title={<>{title} {closeable ? <Close><CloseIcon onClick={handleClick} /></Close> : null}</>}
|
title={<><span style={{ display: 'inline-block' }}>{title}</span> {closeable ?
|
||||||
|
<Close><CloseIcon onClick={handleClick} /></Close> : null}</>}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import { ConfigProvider } from 'antd'
|
import { ConfigProvider } from 'antd'
|
||||||
import { Theme } from './theme'
|
import { Theme } from './theme'
|
||||||
import { ThemeProvider } from './theme/ThemeProvider'
|
import { ThemeProvider } from './theme/ThemeProvider'
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import 'typeface-lato'
|
import 'typeface-lato'
|
||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
@ -13,10 +14,12 @@ const root = ReactDOM.createRoot(
|
|||||||
);
|
);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ConfigProvider theme={Theme}>
|
<BrowserRouter>
|
||||||
<ThemeProvider>
|
<ConfigProvider theme={Theme}>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<App />
|
||||||
</ConfigProvider>
|
</ThemeProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
20
src/pages/activation/home/Home.tsx
Normal file
20
src/pages/activation/home/Home.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { StyledContainer, StyledImageWrapper, StyledMainContent, StyledTitle, StyledContent } from './styled'
|
||||||
|
import { Divider } from 'antd'
|
||||||
|
import { Button } from '../../../components/Button'
|
||||||
|
|
||||||
|
export const Home: FC = () => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledImageWrapper />
|
||||||
|
<StyledMainContent>
|
||||||
|
<StyledTitle level={3}>Welcome to iHealth CheckMeSafe!</StyledTitle>
|
||||||
|
<StyledContent>
|
||||||
|
<Button block href="/activate">Activate Kit</Button>
|
||||||
|
<Divider plain>Or</Divider>
|
||||||
|
<Button block type="default" href="/login">Log in</Button>
|
||||||
|
</StyledContent>
|
||||||
|
</StyledMainContent>
|
||||||
|
</StyledContainer>
|
||||||
|
)
|
||||||
|
}
|
BIN
src/pages/activation/home/images/homePic@2x.png
Normal file
BIN
src/pages/activation/home/images/homePic@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 MiB |
1
src/pages/activation/home/index.ts
Normal file
1
src/pages/activation/home/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Home'
|
57
src/pages/activation/home/styled.tsx
Normal file
57
src/pages/activation/home/styled.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import Image from './images/homePic@2x.png'
|
||||||
|
import { Title } from '../../../components/Typography'
|
||||||
|
|
||||||
|
export const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 25px 0 25px 40px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
padding: 4px 23px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const StyledImageWrapper = styled.div`
|
||||||
|
width: 54%;
|
||||||
|
max-width: 737px;
|
||||||
|
height: 773px;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url(${Image});
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
width: 100%;
|
||||||
|
height: 393px;
|
||||||
|
max-width: 344px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledMainContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 26px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledTitle = styled(Title)`
|
||||||
|
`
|
||||||
|
export const StyledContent = styled.div`
|
||||||
|
margin-top: 44px;
|
||||||
|
width: 358px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 26px;
|
||||||
|
}
|
||||||
|
`
|
209
src/pages/activation/login/ForgetPasswordModal.tsx
Normal file
209
src/pages/activation/login/ForgetPasswordModal.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { Modal, ModalProps } from '../../../components/Modal'
|
||||||
|
import { Title, Paragraph, Link } from '../../../components/Typography'
|
||||||
|
import { Form } from 'antd'
|
||||||
|
import { Button } from '../../../components/Button'
|
||||||
|
import { Input, Password, ErrorMessage, LabelWithTooltip } from '../../../components/FormControl'
|
||||||
|
import Image from './images/password@2x.png'
|
||||||
|
|
||||||
|
// TODO: Captcha
|
||||||
|
type Props = Pick<ModalProps, 'open'> & {
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 366px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.up('s')} {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const StyledMain = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.up('s')} {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
padding-top: 37px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item-required {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const StyledTitle = styled(Title)`
|
||||||
|
margin-bottom: 2px !important;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
margin-bottom: 5px !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const StyledCaptchaHint = styled(Paragraph)`
|
||||||
|
margin-top: 30px;
|
||||||
|
`
|
||||||
|
const TitleCopies = [
|
||||||
|
'Forgot Password',
|
||||||
|
'Reset Password',
|
||||||
|
'Reset Password',
|
||||||
|
'Password Reset'
|
||||||
|
]
|
||||||
|
|
||||||
|
const DescriptionCopies = [
|
||||||
|
'Enter your email address to reset your password.',
|
||||||
|
'A 6-digit code has been sent to John.smith@gmail.com',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ForgetPasswordModal: FC<Props> = ({ onClose, ...props }) => {
|
||||||
|
const [step, setStep] = useState(0)
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [captcha, setCaptcha] = useState('')
|
||||||
|
const [expiration, setExpiration] = useState('9:45')
|
||||||
|
const handleFinishEmail = ({ email }: { email: string }) => {
|
||||||
|
setEmail(email)
|
||||||
|
setStep(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinishCaptcha = ({ captcha }: { captcha: string }) => {
|
||||||
|
setCaptcha(captcha)
|
||||||
|
setStep(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendAgain = () => {
|
||||||
|
// TODO: Update expiration
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = (data: { password: string }) => {
|
||||||
|
console.log(data)
|
||||||
|
setStep(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setStep(0)
|
||||||
|
setEmail('')
|
||||||
|
setCaptcha('')
|
||||||
|
|
||||||
|
onClose && onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
// TODO: login
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal {...props} width={688} destroyOnClose onCancel={handleClose}>
|
||||||
|
<StyledWrapper>
|
||||||
|
<StyledMain>
|
||||||
|
<StyledTitle level={2}>{TitleCopies[step]}</StyledTitle>
|
||||||
|
{step < 2 && <Paragraph>{DescriptionCopies[step]}</Paragraph>}
|
||||||
|
{step === 0 ? (
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
style={{ marginTop: 4 }}
|
||||||
|
onFinish={handleFinishEmail}>
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true, message: <ErrorMessage message="Please input your email" />
|
||||||
|
},
|
||||||
|
{ type: 'email', message: <ErrorMessage message="Invalid email" /> }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="email@example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button style={{ marginTop: 20 }} block htmlType="submit">Next</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
{step === 1 ? (
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
style={{ marginTop: 27 }}
|
||||||
|
onFinish={handleFinishCaptcha}>
|
||||||
|
<Form.Item
|
||||||
|
name="captcha"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true, message: <ErrorMessage message="Please input your code" />
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<StyledCaptchaHint>
|
||||||
|
The code will expire in {expiration}<br />
|
||||||
|
Didn’t receive the code? <Link onClick={handleSendAgain}>Send again</Link>
|
||||||
|
</StyledCaptchaHint>
|
||||||
|
<Form.Item>
|
||||||
|
<Button block htmlType="submit">Verify</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
{step === 2 ? (
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleConfirm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<LabelWithTooltip
|
||||||
|
primary
|
||||||
|
title='The minimum password length is 8 characters and must contain at least 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special character (!@#$%^&*).'
|
||||||
|
>
|
||||||
|
Please enter a new password
|
||||||
|
</LabelWithTooltip>
|
||||||
|
}
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: <ErrorMessage message="Please input your password" /> },
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/,
|
||||||
|
message: <ErrorMessage message="Invalid password" />
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Password placeholder="********" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Confirm password"
|
||||||
|
name="repeatPassword"
|
||||||
|
dependencies={['password']}
|
||||||
|
rules={[{ required: true, message: <ErrorMessage message="Please confirm your password" /> },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('password') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('The two passwords that you entered do not match'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Password placeholder="********" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button style={{ marginTop: 17 }} block htmlType="submit">Confirm</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
{step === 3 ? (
|
||||||
|
<>
|
||||||
|
<Paragraph style={{ marginTop: 20, marginBottom: 16 }}>
|
||||||
|
Your password has just been reset, please log in to continue.
|
||||||
|
</Paragraph>
|
||||||
|
<img src={Image} style={{ width: 232, height: 83, margin: 'auto', display: 'block' }} />
|
||||||
|
<Button block style={{ marginTop: 20 }} onClick={handleLogin}>Log in</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</StyledMain>
|
||||||
|
</StyledWrapper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
75
src/pages/activation/login/Login.tsx
Normal file
75
src/pages/activation/login/Login.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { FC, useState } from 'react'
|
||||||
|
import {
|
||||||
|
StyledContainer,
|
||||||
|
StyledImageWrapper,
|
||||||
|
StyledMainContent,
|
||||||
|
StyledHeadline,
|
||||||
|
StyledContent,
|
||||||
|
StyledButton,
|
||||||
|
StyledTitle, StyledHint, StyledHelp
|
||||||
|
} from './styled'
|
||||||
|
import { Form } from 'antd'
|
||||||
|
import { Paragraph, Link } from '../../../components/Typography'
|
||||||
|
import { Input, Password, ErrorMessage } from '../../../components/FormControl'
|
||||||
|
import { ForgetPasswordModal } from './ForgetPasswordModal'
|
||||||
|
|
||||||
|
export const Login: FC = () => {
|
||||||
|
const [showModal, setModal] = useState(false)
|
||||||
|
const handleShowModal = () => {
|
||||||
|
setModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setModal(false)
|
||||||
|
}
|
||||||
|
const onFinish = (values: any) => {
|
||||||
|
// TODO: login
|
||||||
|
console.log('Success:', values);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledImageWrapper />
|
||||||
|
<StyledMainContent>
|
||||||
|
<StyledHeadline level={1} style={{ fontSize: 36 }}>Welcome Back!</StyledHeadline>
|
||||||
|
<StyledTitle level={3}>Log in to continue</StyledTitle>
|
||||||
|
<StyledContent>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true, message: <ErrorMessage message="Please input your email" />
|
||||||
|
},
|
||||||
|
{ type: 'email', message: <ErrorMessage message="Invalid email" /> }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="email@example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: <ErrorMessage message="Please input your password" /> }]}
|
||||||
|
extra={<StyledHelp type="text" onClick={handleShowModal}>Forgot password</StyledHelp>}
|
||||||
|
>
|
||||||
|
<Password placeholder="********" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<StyledButton block htmlType="submit">Log in</StyledButton>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<StyledHint>
|
||||||
|
<Paragraph>
|
||||||
|
Don’t have an account?
|
||||||
|
<Link href="/activate">Activate your kit</Link>
|
||||||
|
</Paragraph>
|
||||||
|
</StyledHint>
|
||||||
|
</StyledContent>
|
||||||
|
</StyledMainContent>
|
||||||
|
<ForgetPasswordModal open={showModal} onClose={handleCloseModal} />
|
||||||
|
</StyledContainer>
|
||||||
|
)
|
||||||
|
}
|
BIN
src/pages/activation/login/images/loginPic@2x.png
Normal file
BIN
src/pages/activation/login/images/loginPic@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
src/pages/activation/login/images/password@2x.png
Normal file
BIN
src/pages/activation/login/images/password@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
1
src/pages/activation/login/index.ts
Normal file
1
src/pages/activation/login/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Login'
|
80
src/pages/activation/login/styled.tsx
Normal file
80
src/pages/activation/login/styled.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import Image from './images/loginPic@2x.png'
|
||||||
|
import { Title } from '../../../components/Typography'
|
||||||
|
import { Button } from '../../../components/Button'
|
||||||
|
|
||||||
|
export const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 25px 0 25px 40px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export const StyledImageWrapper = styled.div`
|
||||||
|
width: 54%;
|
||||||
|
max-width: 737px;
|
||||||
|
height: 803px;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url(${Image});
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledMainContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: auto;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledHeadline = styled(Title)`
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
font-size: 36px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledTitle = styled(Title)`
|
||||||
|
margin-top: 0 !important;
|
||||||
|
`
|
||||||
|
export const StyledContent = styled.div`
|
||||||
|
margin-top: 52px;
|
||||||
|
width: 358px;
|
||||||
|
|
||||||
|
${props => props.theme.breakpoints.down('s')} {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 47px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledButton = styled(Button)`
|
||||||
|
margin-top: 30px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledHelp = styled(Button)`
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const StyledHint = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 33px;
|
||||||
|
`
|
@ -18,6 +18,7 @@ html, body {
|
|||||||
.ant-btn-link, .ant-btn-text {
|
.ant-btn-link, .ant-btn-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-wave {
|
.ant-wave {
|
||||||
@ -48,6 +49,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.health-tooltip-highlighted.ant-tooltip .ant-tooltip-inner {
|
.health-tooltip-highlighted.ant-tooltip .ant-tooltip-inner {
|
||||||
|
display: inline-flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 1px 1px 0px 0 #FF5A0C, -1px -1px 0px 0 #FF5A0C, -1px 1px 0px 0 #FF5A0C, 1px -1px 0px 0 #FF5A0C;
|
box-shadow: 1px 1px 0px 0 #FF5A0C, -1px -1px 0px 0 #FF5A0C, -1px 1px 0px 0 #FF5A0C, 1px -1px 0px 0 #FF5A0C;
|
||||||
}
|
}
|
||||||
@ -58,6 +60,7 @@ html, body {
|
|||||||
|
|
||||||
.health-tooltip.ant-tooltip .ant-tooltip-inner {
|
.health-tooltip.ant-tooltip .ant-tooltip-inner {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
box-shadow: 0 2px 0px 0 #000022, 0 2px 0px 0px #000022, 1px 1px 0px 2px #000022;
|
box-shadow: 0 2px 0px 0 #000022, 0 2px 0px 0px #000022, 1px 1px 0px 2px #000022;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,3 +194,27 @@ html, body {
|
|||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 500px) {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.ant-form-item .ant-form-item-explain-error {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ant-modal .ant-modal-content {
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item .ant-form-item-label > label .ant-form-item-tooltip {
|
||||||
|
color: #000022;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ThemeConfig } from 'antd'
|
import { ThemeConfig } from 'antd'
|
||||||
|
|
||||||
export const breakpointList = {
|
export const breakpointList = {
|
||||||
xs: 0,
|
xs: 0,
|
||||||
s: 500,
|
s: 500,
|
||||||
@ -53,6 +54,11 @@ export const Theme: ThemeConfig = {
|
|||||||
colorBgTextHover: 'transparent',
|
colorBgTextHover: 'transparent',
|
||||||
colorBgTextActive: 'transparent',
|
colorBgTextActive: 'transparent',
|
||||||
},
|
},
|
||||||
|
Divider: {
|
||||||
|
colorSplit: '#000',
|
||||||
|
colorText: '#000',
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
Modal: {
|
Modal: {
|
||||||
borderRadiusLG: 10,
|
borderRadiusLG: 10,
|
||||||
boxShadow: '2px 2px 4px 1px rgba(0, 0, 0, 0.2)',
|
boxShadow: '2px 2px 4px 1px rgba(0, 0, 0, 0.2)',
|
||||||
|
Loading…
Reference in New Issue
Block a user