feat: login & home

This commit is contained in:
EleanorMao 2023-04-24 23:24:17 +08:00
parent c1b7cdcbfa
commit 06358334c4
23 changed files with 605 additions and 23 deletions

View File

@ -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>
); );
} }

View File

@ -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

View 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}</>
}

View 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>
)
}

View File

@ -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'

View File

@ -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) => (

View File

@ -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>

View 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>
)
}

View File

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

View File

@ -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}</>}
/> />
) )
} }

View File

@ -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>
); );

View 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>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

View 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;
}
`

View 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 />
Didnt 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>
)
}

View 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>
Dont have an account?&nbsp;
<Link href="/activate">Activate your kit</Link>
</Paragraph>
</StyledHint>
</StyledContent>
</StyledMainContent>
<ForgetPasswordModal open={showModal} onClose={handleCloseModal} />
</StyledContainer>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

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

View 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;
`

View File

@ -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;
}

View File

@ -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)',