Compare commits

...

12 Commits

Author SHA1 Message Date
b096240432 feat: dashboard 2023-05-01 23:51:24 +08:00
837528fd08 feat: history 2023-05-01 23:39:21 +08:00
208a0ec475 feat: menu 2023-05-01 21:26:45 +08:00
1931dbd152 feat; munu stryle 2023-05-01 21:22:14 +08:00
97fdafc657 feat: menu 2023-05-01 21:06:49 +08:00
1c232948ed feat: active 2023-04-30 23:07:43 +08:00
abc53d3eef feat: activate 2023-04-25 21:33:35 +08:00
06358334c4 feat: login & home 2023-04-24 23:24:17 +08:00
c1b7cdcbfa feat: add icons 2023-04-24 09:28:33 +08:00
0a043842d5 feat: add button style 2023-04-24 09:19:23 +08:00
4c0628612f feat: add button type 2023-04-24 09:17:51 +08:00
c196059515 feat: add link 2023-04-24 00:01:58 +08:00
94 changed files with 3488 additions and 100 deletions

37
NOTE.md
View File

@ -1,37 +0,0 @@
# Responsive
# Basic Components
* [x] Typography
* [x] Link
* [x] Button
* [x] IconButton
* [x] TextButton
* [x] Modal
* [x] Step
* [x] Tooltip
* [x] Tag
# HandWrite Component
* Hamburger Menu - 需要手写!
* Captcha Input - 需要找别的组件!
# Form Components
* [x] Input
* [x] Password Input with Eye Icon
* [x] Radio - 大屏上黑色主题
* [x] Radio Group
* [x] Checkbox
* [x] Textarea
* [x] Select
* [x] Form
* [x] FormItem
* [x] FormItem with Error Message
# Layout Components
* Layout
* [x] Header
* [x] Footer

View File

@ -16,6 +16,7 @@
"react-scripts": "5.0.1",
"styled-components": "^5.3.9",
"typeface-lato": "^1.1.13",
"typeface-roboto": "^1.1.13",
"typescript": "^4.9.5",
"ua-parser-js": "^1.0.35"
},

View File

@ -1,13 +1,11 @@
import React from 'react';
import { AppRoutes } from './routes/AppRoutes'
import { BreakpointProvider } from './components/Breakpoint'
import { Header } from './components/Layout/Header'
import { Footer } from './components/Layout/Footer'
function App() {
return (
<BreakpointProvider>
<Header showActiveKit showMenu />
<Footer />
<AppRoutes />
</BreakpointProvider>
);
}

View File

@ -66,7 +66,7 @@ export const useBreakpoint = (props: BreakpointProps): boolean => {
return size.width === breakpointList[key as keyof typeof breakpointList]
}
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') {
return key === orientation

View File

@ -2,7 +2,7 @@ import React, { FC } from 'react'
import { Button as AntdButton, ButtonProps as AntdButtonProps } from 'antd'
export type ButtonProps = Omit<AntdButtonProps, 'danger' | 'ghost' | 'shape' | 'size' | 'type'> & {
type?: 'primary' | 'link' | 'text'
type?: 'primary' | 'default' | 'link' | 'text'
}
export const Button: FC<ButtonProps> = (props) => {
return <AntdButton type="primary" {...props} />

View File

@ -0,0 +1,135 @@
import React, { ChangeEvent, FC, FocusEvent, KeyboardEvent, MouseEvent, RefObject } from 'react'
import styled from 'styled-components'
export interface CaptchaProps {
length?: number
value?: string
onChange?: (value: string) => void
}
const StyledWrapper = styled.div`
display: flex;
flex-direction: row;
gap: 14px;
`
const StyledInputWrapper = styled.div`
flex: 0 0 40px;
height: 48px;
`
const StyledInput = styled.input`
width: 100%;
height: 48px;
padding: 8px;
font-size: 28px;
font-weight: 500;
line-height: 32px;
text-align: center;
border-radius: 2px;
box-sizing: border-box;
border: 2px solid #000022;
&:focus {
text-decoration: underline;
outline: 2px solid ${({ theme }) => theme.brandPrimary};
}
`
function textSelect(element: HTMLInputElement): void {
const start = 0;
const end = element.value.length;
element.setSelectionRange(start, end);
element.focus();
}
export const Captcha: FC<CaptchaProps> = ({ value, onChange, length = 6 }) => {
const inputDomRefs: RefObject<HTMLInputElement>[] = Array.from({ length }, () => React.createRef<HTMLInputElement>())
const getCode = (index: number): string => {
return value && value.length > index ? value[index] : ''
}
const getNewValue = (newValue: string, index: number) => {
const oldValue = Array.from({ length }, (_, i) => getCode(i))
oldValue[index] = newValue
return oldValue.map(v => v || ' ').join('').trimEnd()
}
const focusOn = (index: number, select?: boolean) => {
if (inputDomRefs[index] && inputDomRefs[index].current) {
inputDomRefs[index].current?.focus()
if (select) {
textSelect(inputDomRefs[index].current!)
}
}
}
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
textSelect(e.target)
}
const handleClick = (e: MouseEvent<HTMLInputElement>) => {
textSelect(e.target as HTMLInputElement)
}
const handleChange = (index: number) => (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.trim()
if (onChange) {
onChange(getNewValue(value, index))
}
textSelect(e.target)
if (value) {
focusOn(index + 1)
}
}
const handleKeydown = (index: number) => (e: KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'Backspace':
// @ts-ignore
if (e.target.value === "") {
e.stopPropagation();
focusOn(index - 1)
}
break;
case 'ArrowLeft':
case 'ArrowUp':
e.stopPropagation();
if (inputDomRefs[index - 1]) {
focusOn(index - 1)
} else {
focusOn(index)
}
break;
case 'ArrowRight':
case 'ArrowDown':
e.stopPropagation();
if (inputDomRefs[index + 1]) {
focusOn(index + 1)
} else {
focusOn(index)
}
break;
default:
textSelect(e.target as HTMLInputElement)
break;
}
}
return (
<StyledWrapper>
{
Array.from({ length }, () => "").map((_, index) => {
return (
<StyledInputWrapper key={index}>
<StyledInput
type="text"
value={getCode(index)}
ref={inputDomRefs[index]}
onFocus={handleFocus}
onClick={handleClick}
onChange={handleChange(index)}
onKeyDown={handleKeydown(index)}
spellCheck="false"
autoComplete="false"
autoCorrect="false"
autoCapitalize="off"
/>
</StyledInputWrapper>
)
})
}
</StyledWrapper>
)
}

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: 12px;
margin-right: 4px;
line-height: 18px;
}
`
export const ErrorMessage: FC<PropsWithChildren<{ message?: string }>> = ({ message, children }) => {
return <><StyledIcon />{children || message}</>
}

View File

@ -0,0 +1,41 @@
import React, { FC, MouseEvent, 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 StyledSpan = styled.span`
right: 0;
color: #000022;
font-size: 16px;
position: absolute;
`
const Span: FC<PropsWithChildren<{ onClick?: (e: MouseEvent) => void }>> = ({ onClick, children }) => {
const handleClick = (e: any) => {
e.preventDefault()
onClick && onClick(e)
}
return <StyledSpan onClick={handleClick}>{children}</StyledSpan>
}
export const LabelWithTooltip: FC<PropsWithChildren<{ title: string; primary?: boolean }>> = ({
primary,
children,
title
}) => {
return (
<StyledLabel>
{children}
<Tooltip closeable title={title} primary={primary} placement="left">
<Span>
<InfoCircleOutlined />
</Span>
</Tooltip>
</StyledLabel>
)
}

View File

@ -5,3 +5,6 @@ export * from './Select'
export * from './Chekbox'
export * from './Radio'
export * from './RadioGroup'
export * from './ErrorMessage'
export * from './LabelWithTooltip'
export * from './Captcha'

View File

@ -2,24 +2,25 @@ import React, { FC, PropsWithChildren } from 'react'
import styled from 'styled-components'
import { Breakpoint } from '../Breakpoint'
import { ButtonText } from '../Typography'
import { ExternalPaths } from 'routes/Paths'
const CompanyLink = {
title: 'iHealth Labs Inc',
href: 'https://ihealthlabs.com/'
href: ExternalPaths.CompanyLink
}
const FooterLinks = [{
title: 'FAQs',
href: 'https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2-detail',
href: ExternalPaths.Faqs,
}, {
title: 'Contact Us',
href: 'mailto:support@ihealthlabs.com'
href: ExternalPaths.ContactUs
}, {
title: 'Terms of Use',
href: 'https://www.iHealthCheckMeSafe.com/TermsOfUse'
href: ExternalPaths.TermsOfUse
}, {
title: 'Privacy Policy',
href: 'https://www.iHealthCheckMSafe.com/PrivacyPolicy'
href: ExternalPaths.PrivacyPolicy
}]
const Links = styled.div`
@ -119,12 +120,12 @@ const TinyFooter = styled.footer`
const StyledFooter: FC<PropsWithChildren> = ({ children }) => {
return (
<>
<Breakpoint s down>
<Breakpoint width={{ max: 520 }}>
<TinyFooter>
{children}
</TinyFooter>
</Breakpoint>
<Breakpoint s up>
<Breakpoint width={{ min: 520 }}>
<LargeFooter>
{children}
</LargeFooter>
@ -133,9 +134,9 @@ const StyledFooter: FC<PropsWithChildren> = ({ children }) => {
)
}
export const Footer: FC = () => {
export const Footer: FC<{ className?: string }> = (props) => {
return (
<StyledFooter>
<StyledFooter {...props}>
<Links>
{
FooterLinks.map((link, index) => (

View File

@ -3,9 +3,10 @@ import styled from 'styled-components'
import Icon from '@ant-design/icons'
import { Button } from '../Button'
import { Breakpoint } from '../Breakpoint'
import { ReactComponent as Cart } from '../../icons/cart.svg'
import { ReactComponent as Menu } from '../../icons/menu.svg'
import { ReactComponent as Logo } from '../../branding/logo.svg'
import { ReactComponent as Cart } from 'icons/cart.svg'
import { ReactComponent as Menu } from 'icons/menu.svg'
import { ReactComponent as Logo } from 'branding/logo.svg'
import { ExternalPaths } from 'routes/Paths'
const StyledHeader = styled.header`
display: flex;
@ -20,7 +21,6 @@ const StyledHeader = styled.header`
border-bottom: 2px solid #000022;
}
`
const StyledLogo = styled(Logo)`
width: 171px;
height: 41px;
@ -75,14 +75,16 @@ const StyledButton = styled(Button)`
right: 156px;
top: 45px;
width: 216px;
line-height: 58px;
height: 58px;
${props => props.theme.breakpoints.down(600)} {
width: 160px;
width: 130px;
}
`
export interface HeaderProps {
className?: string
showMenu?: boolean
onClickMenu?: () => void
showActiveKit?: boolean;
@ -101,10 +103,11 @@ export const Header: FC<HeaderProps> = ({
showMenu,
onClickMenu,
showActiveKit,
activeKitHref
activeKitHref,
className
}) => {
return (
<StyledHeader>
<StyledHeader className={className}>
{showMenu &&
<Breakpoint s down>
<StyledMenuIcon component={Menu} onClick={onClickMenu} />
@ -115,7 +118,8 @@ export const Header: FC<HeaderProps> = ({
<Breakpoint s up>
<StyledButton href={activeKitHref}>Activate Kit</StyledButton>
</Breakpoint>}
<StyledCartWrapper target="_blank" rel="noopener noreferrer">
<StyledCartWrapper target="_blank" rel="noopener noreferrer"
href={ExternalPaths.Shop}>
<Icon component={Cart} />
<StyledText>SHOP</StyledText>
</StyledCartWrapper>

View File

@ -0,0 +1,85 @@
import React, { FC, PropsWithChildren, useState } from 'react'
import styled from 'styled-components'
import { Header, HeaderProps } from './Header'
import { Footer } from './Footer'
import { Menu } from '../Menu'
import { Breakpoint, useBreakpoint } from '../Breakpoint'
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;
`
const StyledMainSection = styled.section`
flex: auto;
display: flex;
flex-direction: column;
`
const StyledMainWrapper = styled.section<{ $column?: boolean }>`
flex: auto;
display: flex;
flex-direction: ${({ $column }) => $column ? 'column' : 'row'};
`
export const Main: FC<PropsWithChildren<{ column?: boolean }>> = ({ children, column }) => {
return (
<>
<Breakpoint s down>
{children}
</Breakpoint>
<Breakpoint s up>
<StyledMainWrapper $column={column}>
{children}
</StyledMainWrapper>
</Breakpoint>
</>
)
}
export const Layout: FC<PropsWithChildren<Omit<HeaderProps, 'className' | 'onClickMenu'>>> = ({
children,
...headerProps
}) => {
const isSmallScreen = useBreakpoint({ s: true, down: true })
const [displayMenu, setDisplayMenu] = useState(false)
const handleClickMenu = () => {
if (!isSmallScreen) return
setDisplayMenu(s => !s)
}
const handleCloseMenu = () => {
if (!isSmallScreen) return
setDisplayMenu(false)
}
const handleLogout = () => {}
return (
<StyledSection>
<StyledHeader {...headerProps} onClickMenu={handleClickMenu} />
<Main>
{headerProps.showMenu && (!isSmallScreen || displayMenu) ?
<Menu onClose={handleCloseMenu} onLogout={handleLogout} /> : null}
<StyledMainSection>
<StyledMain>
{children}
</StyledMain>
<StyledFooter />
</StyledMainSection>
</Main>
</StyledSection>
)
}

View File

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

View File

@ -0,0 +1,62 @@
import React, { FC, useState } from 'react'
import { Breakpoint } from '../Breakpoint'
import {
StyledActivateButton, StyledBackground, StyledCloseIcon, StyledCopyright, StyledExtraContent,
StyledIcon,
StyledMenuContainer,
StyledMenuItem, StyledNextIcon,
StyledPlainMenuContainer, StyledPlainMenuItem
} from './styled'
import { Paths } from 'routes/Paths'
import { menuConfig } from 'routes/menuConfig'
import { Text } from '../Typography'
import { ReactComponent as CloseSvg } from 'icons/close.svg'
import { ReactComponent as CaretRightSvg } from 'icons/caretRight.svg'
export interface MenuProps {
onClose: () => void
onLogout: () => void
}
export const Menu: FC<MenuProps> = ({ onLogout, onClose }) => {
const [copyright] = useState(`Copyright iHealth ${new Date().getFullYear()}. All Rights Rerved.`)
return (
<>
<Breakpoint s down>
<StyledBackground>
<StyledMenuContainer>
<StyledCloseIcon component={CloseSvg} onClick={onClose} />
<StyledActivateButton block href={Paths.Activate}>Activate Kit</StyledActivateButton>
{menuConfig.map((item) => (
<StyledMenuItem key={item.title} to={item.href} target={item.target}
rel={item.target === '_blank' ? "noopener noreferrer" : undefined}>
<StyledIcon component={item.icon} />
{item.title}
</StyledMenuItem>
))}
<StyledExtraContent>
<StyledCopyright>{copyright}</StyledCopyright>
<Text primary style={{ lineHeight: 1 }} onClick={onLogout}>Log out</Text>
</StyledExtraContent>
</StyledMenuContainer>
</StyledBackground>
</Breakpoint>
<Breakpoint s up>
<StyledPlainMenuContainer>
{menuConfig.map((item) => (
<StyledPlainMenuItem key={item.title} to={item.href} target={item.target}
rel={item.target === '_blank' ? "noopener noreferrer" : undefined}>
<StyledIcon component={item.icon} />
{item.title}
<StyledNextIcon component={CaretRightSvg} />
</StyledPlainMenuItem>
))}
<StyledExtraContent>
<StyledCopyright>{copyright}</StyledCopyright>
<Text primary style={{ lineHeight: 1 }} onClick={onLogout}>Log out</Text>
</StyledExtraContent>
</StyledPlainMenuContainer>
</Breakpoint>
</>
)
}

View File

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

View File

@ -0,0 +1,116 @@
import styled from 'styled-components'
import { Button } from '../Button'
import Icon from '@ant-design/icons'
import { NavLink } from 'react-router-dom'
export const StyledExtraContent = styled.div`
text-align: center;
`
export const StyledBackground = styled.div`
background: rgba(0, 0, 34, 0.5);
position: fixed;
top: 66px;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
pointer-events: none;
`
export const StyledMenuContainer = styled.aside`
background: #FDFDFD;
border: 2px solid #000022;
box-shadow: 3px 6px 1px #000022;
padding: 60px 20px 20px;
width: 316px;
box-sizing: border-box;
position: absolute;
pointer-events: auto;
top: 0;
left: 0;
bottom: 0;
${StyledExtraContent} {
position: absolute;
padding: 20px;
left: 0;
bottom: 0;
right: 0;
}
`
export const StyledActivateButton = styled(Button)`
border-radius: 8px;
margin-bottom: 11px;
`
export const StyledMenuItem = styled(NavLink)`
height: 64px;
line-height: 64px;
text-align: center;
border-bottom: 1px solid #000022;
font-weight: 500;
font-size: 16px;
letter-spacing: 0.02em;
color: #000022;
text-decoration: none;
display: block;
`
export const StyledIcon = styled(Icon)`
margin-right: 6px;
`
export const StyledCloseIcon = styled(Icon)`
position: absolute;
top: 23px;
right: 23px;
`
export const StyledCopyright = styled.div`
color: #1E1D1F;
font-weight: 400;
line-height: 14px;
font-size: 12px;
margin-bottom: 15px;
`
export const StyledPlainMenuContainer = styled.aside`
width: 268px;
box-sizing: border-box;
background: #F3F3F3;
padding-top: 5px;
flex: 0 0 auto;
${StyledExtraContent} {
padding: 37px 7px;
${StyledCopyright} {
margin-bottom: 19px;
}
}
`
export const StyledPlainMenuItem = styled(NavLink)`
height: 64px;
line-height: 64px;
padding: 0 20px;
color: #000022;
font-weight: 500;
font-size: 16px;
display: block;
letter-spacing: 0.02em;
position: relative;
border-bottom: 2px solid #FDFDFD;
text-decoration: none;
&:focus, &:active, &.active {
color: ${props => props.theme.brandPrimary};
}
`
export const StyledNextIcon = styled(Icon)`
width: 9px;
height: 14px;
position: absolute;
right: 24px;
top: 50%;
transform: translate3d(0, -50%, 0);
`

View File

@ -2,42 +2,67 @@ import React, { FC } from 'react'
import { Steps as AntdSteps, StepsProps as AntdStepsProps } from 'antd'
import { StepProps } from 'antd/es/steps'
import styled from 'styled-components'
import {StepIcon} from './StepIcon'
import { StepIcon } from './StepIcon'
import { useBreakpoint } from '../Breakpoint'
export type StepsProps = Omit<AntdStepsProps, 'labelPlacement' | 'size' | 'status' | 'type' | 'items'> & {
items: Omit<StepProps, 'icon' | 'description'>[]
showBar?: boolean
barStyle?: React.CSSProperties
}
const StyledSteps = styled(AntdSteps)`
&.ant-steps-label-vertical {
.ant-steps-item-content {
margin-top: 5px;
}
.ant-steps-item-content {
margin-top: 5px;
}
.ant-steps-item-tail {
top: 8px;
margin-inline-start: 53px;
padding: 4px 12px;
}
.ant-steps-item-tail {
top: 8px;
margin-inline-start: 53px;
padding: 4px 12px;
}
}
.ant-steps-item-title {
font-size: 12px;
line-height: 14px;
font-size: 12px;
line-height: 14px;
}
.ant-steps-item-finish > .ant-steps-item-container > .ant-steps-item-content > .ant-steps-item-title {
color: #8F9296;
color: #8F9296;
}
`
export const StyledBar = styled.div`
height: 5px;
margin-bottom: 9px;
position: relative;
&:after {
content: '';
position: absolute;
height: 5px;
width: 134px;
background: #000022;
border-radius: 5px;
top: 0;
left: 50%;
transform: translate3d(-50%, 0, 0);
}
`
export const Steps: FC<StepsProps> = ({ items, current = 0, ...props }) => {
export const Steps: FC<StepsProps> = ({ items, current = 0, barStyle, showBar, ...props }) => {
const formattedItems = items.map((item, index) => {
return {
...item,
icon: <StepIcon active={index <= current}>{index + 1}</StepIcon>
}
})
const isSmallScreen = useBreakpoint({ s: true, down: true })
return (
<StyledSteps labelPlacement="vertical" {...props} current={current} items={formattedItems} />
<>
<StyledSteps responsive={false} labelPlacement="vertical" {...props} current={current} items={formattedItems} />
{isSmallScreen || showBar ? <StyledBar style={barStyle} /> : null}
</>
)
}

View File

@ -4,16 +4,19 @@ import {TinyText} from '../Typography'
const StyledTag = styled.div`
background: #FFAB83;
border-radius: 7px;
border-radius: 12px;
text-align: center;
height: 14px;
line-height: 14px;
height: 22px;
line-height: 22px;
padding: 0 10px;
vertical-align: bottom;
display: inline-block;
> span {
position: relative;
vertical-align: bottom;
display: inline-block;
top: -4px;
line-height: 22px;
}
`
export const Tag: React.FC<PropsWithChildren> = (props) => {

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 { ReactComponent as CloseIcon } from '../../icons/close.svg'
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`
line-height: 17px;
line-height: 25px;
vertical-align: middle;
cursor: pointer;
padding-left: 4px;
display: inline-block;
> svg {
width: 14px;
@ -27,7 +31,8 @@ export const Tooltip: FC<TooltipProps> = ({ closeable, primary, title, overlayCl
trigger="click"
overlayClassName={`health-tooltip${primary ? '-highlighted' : ''} ${overlayClassName || ''}`}
{...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

@ -0,0 +1,16 @@
import React, { FC, ComponentProps } from 'react'
import { Typography, TypographyProps } from 'antd'
import styled from 'styled-components'
type LinkProps = ComponentProps<TypographyProps['Link']> & {
inherit?: boolean
}
const StyledLink = styled(Typography.Link)<{
$inherit?: boolean
}>`
${({ $inherit }) => $inherit ? 'font-size: inherit;line-height:inherit;' : ''}
`
export const Link: FC<LinkProps> = ({ inherit, ...props }) => {
return <StyledLink {...props} $inherit={inherit} />
}

View File

@ -3,15 +3,23 @@ import { Typography, TypographyProps } from 'antd'
import styled from 'styled-components'
type AdditionalParagraphProps = ComponentProps<TypographyProps['Paragraph']> & {
level?: 1 | 2
level?: 1 | 2;
primary?: boolean;
}
const StyledParagraph2 = styled(Typography.Paragraph)`
font-size: 12px;
const StyledParagraph = styled(Typography.Paragraph)<{$primary?:boolean}>`
${({ $primary, theme }) => $primary ? `color: ${theme.brandPrimary}` : ''}
`
export const Paragraph: FC<AdditionalParagraphProps> = ({ level = 1, ...props }) => {
const StyledParagraph2 = styled(Typography.Paragraph)<{
$primary?:boolean
}>`
font-size: 12px;
${({ $primary, theme }) => $primary ? `color: ${theme.brandPrimary}` : ''}
`
export const Paragraph: FC<AdditionalParagraphProps> = ({ level = 1, primary, ...props }) => {
if (level === 2) {
return <StyledParagraph2 {...props} />
return <StyledParagraph2 {...props} $primary={primary} />
}
return <Typography.Paragraph {...props} />
return <StyledParagraph {...props} $primary={primary} />
}

View File

@ -0,0 +1,19 @@
import React, { FC, ComponentProps } from 'react'
import { Typography, TypographyProps } from 'antd'
import styled from 'styled-components'
type TextProps = ComponentProps<TypographyProps['Text']> & {
primary?: boolean
inherit?: boolean
}
const StyledText = styled(Typography.Text)<{
$inherit?: boolean
$primary?: boolean
}>`
${({ $inherit }) => $inherit ? 'font-size: inherit;line-height:inherit;' : ''}
${({ $primary, theme }) => $primary ? `color: ${theme.brandPrimary}` : ''}
`
export const Text: FC<TextProps> = ({ primary, inherit, ...props }) => {
return <StyledText {...props} $primary={primary} $inherit={inherit} />
}

View File

@ -7,7 +7,6 @@ type TinyTextProps = ComponentProps<TypographyProps['Text']>
const StyledTinyText = styled(Typography.Text)`
font-weight: 400;
font-size: 12px;
transform: scale(0.66667);
line-height: 1.75;
color: #1E1D1F;
`

View File

@ -0,0 +1,17 @@
import React, { FC, ComponentProps } from 'react'
import { Typography, TypographyProps } from 'antd'
import styled from 'styled-components'
type AdditionalTitleProps = Omit<ComponentProps<TypographyProps['Title']>, 'level'> & {
level?: 0 | 1 | 2 | 3 | 4 | 5
}
const StyledTitle0 = styled(Typography.Title)`
font-size: 36px!important;
`
export const Title: FC<AdditionalTitleProps> = ({ level = 1, ...props }) => {
if (level === 0) {
return <StyledTitle0 {...props} />
}
return <Typography.Title level={level} {...props} />
}

View File

@ -1,8 +1,7 @@
import { Typography } from 'antd'
const { Text, Title, Link } = Typography
export * from './Paragraph'
export * from './ButtonText'
export * from './TinyText'
export { Text, Title, Link }
export * from './Text'
export * from './Title'
export * from './Link'

2
src/constants/storage.ts Normal file
View File

@ -0,0 +1,2 @@
export const Token = 'token'
export const UserInfo = 'user-info'

4
src/icons/bell.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="24" height="26" viewBox="0 0 24 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9999 2.02502C9.90788 2.02502 7.83999 2.88157 6.36072 4.36085C4.88144 5.84012 4.02489 7.90801 4.02489 10C4.02489 14.6015 3.06305 17.3784 2.13697 18.9852C2.13807 18.9883 2.13958 18.9916 2.14154 18.9951C2.14258 18.9969 2.14464 19 2.14464 19H21.8551C21.8551 19 21.8572 18.9969 21.8582 18.9951C21.8602 18.9916 21.8617 18.9883 21.8628 18.9852C20.9367 17.3784 19.9749 14.6015 19.9749 10C19.9749 7.90801 19.1183 5.84012 17.6391 4.36085C16.1598 2.88157 14.0919 2.02502 11.9999 2.02502ZM4.9465 2.94663C6.80085 1.09229 9.37745 0.0250244 11.9999 0.0250244C14.6223 0.0250244 17.1989 1.09229 19.0533 2.94663C20.9076 4.80098 21.9749 7.37758 21.9749 10C21.9749 14.3334 22.8817 16.7534 23.6028 17.999L23.6034 18C24.3493 19.2919 23.4168 20.9946 21.8784 21L21.8749 21H2.12489L2.12137 21C0.582952 20.9946 -0.349523 19.2919 0.39637 18L0.396965 17.999C1.11809 16.7534 2.02489 14.3334 2.02489 10C2.02489 7.37758 3.09215 4.80098 4.9465 2.94663Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.9999 19C8.55219 19 8.9999 19.4477 8.9999 20V21C8.9999 21.7956 9.31597 22.5587 9.87858 23.1213C10.4412 23.6839 11.2043 24 11.9999 24C12.7956 24 13.5586 23.6839 14.1212 23.1213C14.6838 22.5587 14.9999 21.7956 14.9999 21V20C14.9999 19.4477 15.4476 19 15.9999 19C16.5522 19 16.9999 19.4477 16.9999 20V21C16.9999 22.3261 16.4731 23.5979 15.5354 24.5355C14.5978 25.4732 13.326 26 11.9999 26C10.6738 26 9.40205 25.4732 8.46437 24.5355C7.52669 23.5979 6.9999 22.3261 6.9999 21V20C6.9999 19.4477 7.44762 19 7.9999 19Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

3
src/icons/caretLeft.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7071 5.29289C21.0976 5.68342 21.0976 6.31658 20.7071 6.70711L11.4142 16L20.7071 25.2929C21.0976 25.6834 21.0976 26.3166 20.7071 26.7071C20.3166 27.0976 19.6834 27.0976 19.2929 26.7071L9.29289 16.7071C8.90237 16.3166 8.90237 15.6834 9.29289 15.2929L19.2929 5.29289C19.6834 4.90237 20.3166 4.90237 20.7071 5.29289Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 492 B

3
src/icons/caretRight.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="9" height="14" viewBox="0 0 9 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.05806 0.308058C1.30214 0.0639806 1.69786 0.0639806 1.94194 0.308058L8.19194 6.55806C8.43602 6.80214 8.43602 7.19786 8.19194 7.44194L1.94194 13.6919C1.69786 13.936 1.30214 13.936 1.05806 13.6919C0.813981 13.4479 0.813981 13.0521 1.05806 12.8081L6.86612 7L1.05806 1.19194C0.813981 0.947864 0.813981 0.552136 1.05806 0.308058Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

4
src/icons/check.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2236 9.30973C19.6048 9.70934 19.5899 10.3423 19.1903 10.7236L11.8528 17.7236C11.4662 18.0924 10.8579 18.0921 10.4716 17.723L6.80911 14.223C6.40983 13.8414 6.39547 13.2084 6.77704 12.8091C7.1586 12.4098 7.7916 12.3955 8.19089 12.777L11.1631 15.6174L17.8097 9.27645C18.2093 8.89522 18.8423 8.91012 19.2236 9.30973Z" fill="#000022"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 2C6.92487 2 2 6.92487 2 13C2 19.0751 6.92487 24 13 24C19.0751 24 24 19.0751 24 13C24 6.92487 19.0751 2 13 2ZM0 13C0 5.8203 5.8203 0 13 0C20.1797 0 26 5.8203 26 13C26 20.1797 20.1797 26 13 26C5.8203 26 0 20.1797 0 13Z" fill="#000022"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

3
src/icons/download.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7813 7.99157V11.4717C12.7813 11.7354 12.6775 11.9883 12.4929 12.1748C12.3083 12.3612 12.0579 12.466 11.7969 12.466H1.95313C1.69205 12.466 1.44167 12.3612 1.25707 12.1748C1.07246 11.9883 0.96875 11.7354 0.96875 11.4717V7.99157C0.96875 7.85971 1.02061 7.73326 1.11291 7.64002C1.20521 7.54679 1.3304 7.49441 1.46094 7.49441C1.59147 7.49441 1.71666 7.54679 1.80897 7.64002C1.90127 7.73326 1.95313 7.85971 1.95313 7.99157V11.4717H11.7969V7.99157C11.7969 7.85971 11.8487 7.73326 11.941 7.64002C12.0333 7.54679 12.1585 7.49441 12.2891 7.49441C12.4196 7.49441 12.5448 7.54679 12.6371 7.64002C12.7294 7.73326 12.7813 7.85971 12.7813 7.99157ZM6.52678 8.34331C6.57249 8.38953 6.62677 8.4262 6.68652 8.45122C6.74627 8.47624 6.81032 8.48912 6.875 8.48912C6.93968 8.48912 7.00373 8.47624 7.06348 8.45122C7.12323 8.4262 7.17751 8.38953 7.22322 8.34331L9.68416 5.85751C9.72989 5.81132 9.76616 5.75648 9.79091 5.69613C9.81566 5.63578 9.8284 5.57109 9.8284 5.50577C9.8284 5.44045 9.81566 5.37576 9.79091 5.31541C9.76616 5.25506 9.72989 5.20022 9.68416 5.15403C9.63843 5.10784 9.58414 5.0712 9.52439 5.0462C9.46465 5.0212 9.40061 5.00834 9.33594 5.00834C9.27127 5.00834 9.20723 5.0212 9.14748 5.0462C9.08773 5.0712 9.03344 5.10784 8.98771 5.15403L7.36719 6.79155V1.03134C7.36719 0.899484 7.31533 0.77303 7.22303 0.679794C7.13073 0.586559 7.00554 0.53418 6.875 0.53418C6.74446 0.53418 6.61927 0.586559 6.52697 0.679794C6.43467 0.77303 6.38281 0.899484 6.38281 1.03134V6.79155L4.76229 5.15403C4.66993 5.06074 4.54467 5.00834 4.41406 5.00834C4.28345 5.00834 4.15819 5.06074 4.06584 5.15403C3.97349 5.24732 3.9216 5.37384 3.9216 5.50577C3.9216 5.6377 3.97349 5.76422 4.06584 5.85751L6.52678 8.34331Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

3
src/icons/gear.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7178 2.66085C11.9925 2.57219 12.2921 2.60651 12.5395 2.755L15.4626 4.50886C15.8209 4.49285 16.1798 4.49286 16.5381 4.50889L19.449 2.7669C19.6945 2.62003 19.9911 2.58528 20.2638 2.67145C22.1741 3.27497 23.9304 4.28646 25.4111 5.63587C25.6234 5.82931 25.7422 6.10468 25.7374 6.39182L25.6802 9.78962C25.8776 10.0927 26.0587 10.4062 26.2228 10.7285L29.186 12.376C29.4355 12.5147 29.6138 12.7535 29.676 13.0321C30.112 14.9847 30.1166 17.0089 29.6895 18.9635C29.6281 19.2445 29.4487 19.4854 29.197 19.6248L26.2224 21.2723C26.0584 21.5944 25.8774 21.9075 25.6802 22.2104L25.7374 25.6082C25.7422 25.8953 25.6234 26.1707 25.4111 26.3641C23.9335 27.7106 22.1846 28.725 20.2822 29.3391C20.0076 29.4278 19.708 29.3935 19.4605 29.245L16.5374 27.4911C16.1791 27.5071 15.8203 27.5071 15.462 27.4911L12.551 29.2331C12.3056 29.38 12.009 29.4147 11.7363 29.3285C9.82599 28.725 8.06968 27.7135 6.58896 26.3641C6.37668 26.1707 6.25783 25.8953 6.26268 25.6081L6.31994 22.218C6.12371 21.9117 5.94278 21.5959 5.7778 21.2718L2.81411 19.624C2.5646 19.4853 2.38627 19.2465 2.32407 18.9679C1.88811 17.0153 1.88351 14.9911 2.31059 13.0365C2.37199 12.7555 2.55142 12.5146 2.80304 12.3752L5.77768 10.7277C5.94167 10.4056 6.12265 10.0925 6.31988 9.78961L6.26268 6.39182C6.25784 6.10466 6.37671 5.82929 6.58899 5.63585C8.0666 4.28938 9.81543 3.27496 11.7178 2.66085ZM8.27018 6.82083L8.32489 10.0707C8.32844 10.2812 8.26543 10.4875 8.14485 10.6601C7.87663 11.0441 7.6418 11.4504 7.44295 11.8745C7.35355 12.0652 7.20625 12.2228 7.02204 12.3248L4.17771 13.9001C3.92892 15.2891 3.93237 16.7117 4.18791 18.0995L7.02347 19.676C7.20629 19.7776 7.35263 19.9341 7.4419 20.1232C7.64511 20.5539 7.88214 20.9677 8.15075 21.3609C8.26754 21.5319 8.32839 21.7349 8.32489 21.9419L8.27021 25.179C9.35109 26.0921 10.5871 26.8038 11.9194 27.2803L14.699 25.6169C14.8751 25.5116 15.0793 25.4629 15.2839 25.4775C15.7607 25.5116 16.2394 25.5116 16.7162 25.4775C16.9212 25.4629 17.1258 25.5117 17.302 25.6175L20.0906 27.2906C21.4185 26.8066 22.6506 26.0918 23.7299 25.1792L23.6752 21.9293C23.6716 21.7188 23.7346 21.5125 23.8552 21.3399C24.1234 20.9559 24.3583 20.5496 24.5571 20.1255C24.6465 19.9348 24.7938 19.7772 24.978 19.6752L27.8224 18.0999C28.0712 16.7109 28.0677 15.2883 27.8122 13.9005L24.9766 12.324C24.793 12.2219 24.6463 12.0647 24.5571 11.8745C24.3583 11.4504 24.1234 11.0441 23.8552 10.6601C23.7346 10.4875 23.6716 10.2812 23.6752 10.0707L23.7299 6.82096C22.649 5.90786 21.413 5.19614 20.0806 4.7197L17.301 6.38308C17.125 6.48843 16.9208 6.53708 16.7162 6.52244C16.2394 6.48833 15.7607 6.48833 15.2839 6.52244C15.0789 6.53711 14.8743 6.48824 14.698 6.38248L11.9095 4.70935C10.5815 5.19334 9.34945 5.90817 8.27018 6.82083ZM16 11C13.2386 11 11 13.2386 11 16C11 18.7614 13.2386 21 16 21C18.7615 21 21 18.7614 21 16C21 13.2386 18.7615 11 16 11ZM9.00003 16C9.00003 12.134 12.134 8.99999 16 8.99999C19.866 8.99999 23 12.134 23 16C23 19.866 19.866 23 16 23C12.134 23 9.00003 19.866 9.00003 16Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

3
src/icons/heart.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3242 0.894449C10.7723 0.343906 10.0248 0.0343085 9.24526 0.0333569C8.4657 0.0324053 7.71747 0.340177 7.16421 0.889371L6.49999 1.50636L5.83526 0.887339C5.28213 0.335757 4.53254 0.0264956 3.75139 0.0275908C2.97024 0.028686 2.22152 0.340048 1.66993 0.893179C1.11835 1.44631 0.809088 2.1959 0.810184 2.97705C0.811279 3.75821 1.12264 4.50693 1.67577 5.05851L6.21308 9.66234C6.25088 9.70072 6.29594 9.73121 6.34563 9.75201C6.39532 9.77282 6.44866 9.78354 6.50253 9.78354C6.5564 9.78354 6.60974 9.77282 6.65943 9.75201C6.70912 9.73121 6.75418 9.70072 6.79198 9.66234L11.3242 5.05851C11.8762 4.50621 12.1863 3.75732 12.1863 2.97648C12.1863 2.19564 11.8762 1.44675 11.3242 0.894449ZM10.7478 4.48773L6.49999 8.79601L2.2496 4.48367C1.8496 4.08367 1.62488 3.54115 1.62488 2.97546C1.62488 2.40978 1.8496 1.86726 2.2496 1.46726C2.6496 1.06726 3.19212 0.842543 3.7578 0.842543C4.32349 0.842543 4.86601 1.06726 5.26601 1.46726L5.27616 1.47742L6.22323 2.35847C6.29841 2.42843 6.3973 2.46733 6.49999 2.46733C6.60269 2.46733 6.70157 2.42843 6.77675 2.35847L7.72382 1.47742L7.73397 1.46726C8.13424 1.06753 8.67691 0.843178 9.2426 0.843559C9.80828 0.84394 10.3506 1.06902 10.7504 1.46929C11.1501 1.86956 11.3745 2.41223 11.3741 2.97792C11.3737 3.5436 11.1486 4.08597 10.7484 4.4857L10.7478 4.48773Z" fill="#FF5200"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

3
src/icons/history.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.125 4.25002V7.64612L11.9469 9.33909C12.089 9.42445 12.1914 9.56279 12.2316 9.72366C12.2717 9.88453 12.2463 10.0548 12.1609 10.1969C12.0756 10.339 11.9372 10.4414 11.7764 10.4816C11.6155 10.5217 11.4453 10.4963 11.3031 10.411L8.17812 8.53596C8.08563 8.48039 8.00911 8.40183 7.95599 8.30791C7.90287 8.21399 7.87497 8.10792 7.875 8.00002V4.25002C7.875 4.08426 7.94085 3.92529 8.05806 3.80808C8.17527 3.69087 8.33424 3.62502 8.5 3.62502C8.66576 3.62502 8.82473 3.69087 8.94194 3.80808C9.05915 3.92529 9.125 4.08426 9.125 4.25002ZM8.5 0.500023C7.51406 0.497567 6.53742 0.690675 5.62661 1.06817C4.71579 1.44566 3.88889 2.00005 3.19375 2.69924C2.62578 3.27424 2.12109 3.82737 1.625 4.40627V3.00002C1.625 2.83426 1.55915 2.67529 1.44194 2.55808C1.32473 2.44087 1.16576 2.37502 1 2.37502C0.83424 2.37502 0.675268 2.44087 0.558058 2.55808C0.440848 2.67529 0.375 2.83426 0.375 3.00002V6.12502C0.375 6.29078 0.440848 6.44975 0.558058 6.56697C0.675268 6.68418 0.83424 6.75002 1 6.75002H4.125C4.29076 6.75002 4.44973 6.68418 4.56694 6.56697C4.68415 6.44975 4.75 6.29078 4.75 6.12502C4.75 5.95926 4.68415 5.80029 4.56694 5.68308C4.44973 5.56587 4.29076 5.50002 4.125 5.50002H2.32812C2.88672 4.84221 3.44297 4.22268 4.07734 3.58049C4.94598 2.71186 6.05133 2.11847 7.25529 1.87446C8.45924 1.63045 9.70841 1.74665 10.8467 2.20853C11.985 2.67041 12.9619 3.45749 13.6555 4.47144C14.349 5.48538 14.7283 6.68121 14.7461 7.90952C14.7639 9.13782 14.4193 10.3441 13.7554 11.3777C13.0915 12.4113 12.1377 13.2263 11.0132 13.7209C9.88879 14.2155 8.6435 14.3678 7.43299 14.1587C6.22249 13.9496 5.10043 13.3885 4.20703 12.5453C4.14732 12.4889 4.07708 12.4448 4.00032 12.4155C3.92356 12.3862 3.84179 12.3724 3.75967 12.3747C3.67754 12.377 3.59668 12.3955 3.5217 12.429C3.44672 12.4626 3.37908 12.5106 3.32266 12.5703C3.26623 12.63 3.22212 12.7003 3.19283 12.777C3.16355 12.8538 3.14967 12.9356 3.152 13.0177C3.15432 13.0998 3.17279 13.1807 3.20636 13.2557C3.23993 13.3306 3.28795 13.3983 3.34766 13.4547C4.23785 14.2948 5.32012 14.9042 6.5 15.2298C7.67989 15.5554 8.92153 15.5873 10.1166 15.3226C11.3116 15.058 12.4237 14.505 13.3559 13.7117C14.288 12.9184 15.0118 11.9091 15.4642 10.7718C15.9165 9.63442 16.0836 8.40366 15.9509 7.18689C15.8182 5.97011 15.3897 4.80431 14.7029 3.79122C14.016 2.77813 13.0916 1.94854 12.0104 1.37485C10.9292 0.801167 9.72399 0.50082 8.5 0.500023Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

3
src/icons/home.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.15717 0.420883C7.38748 0.210599 7.6881 0.0939941 8 0.0939941C8.3119 0.0939941 8.61252 0.210599 8.84283 0.420883C8.84314 0.421162 8.84344 0.421442 8.84375 0.421722L15.0922 6.1L15.101 6.10816C15.2245 6.22481 15.3234 6.36499 15.392 6.52045C15.4605 6.67591 15.4972 6.84351 15.4999 7.01337L15.5 7.02348V14.25C15.5 14.5816 15.3683 14.8995 15.1339 15.1339C14.8995 15.3684 14.5815 15.5 14.25 15.5H10.5C10.1685 15.5 9.85054 15.3684 9.61612 15.1339C9.38169 14.8995 9.25 14.5816 9.25 14.25V10.5H6.75V14.25C6.75 14.5816 6.6183 14.8995 6.38388 15.1339C6.14946 15.3684 5.83152 15.5 5.5 15.5H1.75C1.41848 15.5 1.10054 15.3684 0.866117 15.1339C0.631696 14.8995 0.5 14.5816 0.5 14.25V7.02348L0.500082 7.01337C0.50283 6.84351 0.539539 6.67591 0.608047 6.52045C0.676556 6.36499 0.775478 6.22481 0.89899 6.10816L0.907791 6.1L7.15625 0.421722C7.15656 0.421442 7.15686 0.421162 7.15717 0.420883ZM1.75592 7.01831L7.99846 1.3454L8 1.34399L8.00154 1.3454L14.2441 7.0183C14.2457 7.02015 14.2471 7.02225 14.2481 7.02452C14.2491 7.02689 14.2498 7.02941 14.25 7.03198V14.25H10.5V10.5C10.5 10.1685 10.3683 9.85058 10.1339 9.61616C9.89946 9.38174 9.58152 9.25005 9.25 9.25005H6.75C6.41848 9.25005 6.10054 9.38174 5.86612 9.61616C5.6317 9.85058 5.5 10.1685 5.5 10.5V14.25H1.75V7.03198C1.75022 7.02941 1.75086 7.02689 1.75191 7.02452C1.75291 7.02225 1.75427 7.02015 1.75592 7.01831Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
src/icons/info.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 2C6.92487 2 2 6.92487 2 13C2 19.0751 6.92487 24 13 24C19.0751 24 24 19.0751 24 13C24 6.92487 19.0751 2 13 2ZM0 13C0 5.8203 5.8203 0 13 0C20.1797 0 26 5.8203 26 13C26 20.1797 20.1797 26 13 26C5.8203 26 0 20.1797 0 13Z" fill="#000022"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 6C13.5523 6 14 6.44772 14 7V14C14 14.5523 13.5523 15 13 15C12.4477 15 12 14.5523 12 14V7C12 6.44772 12.4477 6 13 6Z" fill="#000022"/>
<path d="M13 20C13.8284 20 14.5 19.3284 14.5 18.5C14.5 17.6716 13.8284 17 13 17C12.1716 17 11.5 17.6716 11.5 18.5C11.5 19.3284 12.1716 20 13 20Z" fill="#000022"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B

4
src/icons/link.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4525 3.17426L11.9947 5.63207C11.6041 6.02259 10.971 6.02259 10.5804 5.63207C10.1899 5.24155 10.1899 4.60838 10.5804 4.21786L13.0554 1.74286C13.0664 1.73194 13.0775 1.72129 13.0889 1.71089C14.3269 0.582536 15.952 -0.0254256 17.6266 0.0133431C19.3012 0.0521119 20.8965 0.73463 22.0809 1.91907C23.2654 3.1035 23.9479 4.69877 23.9867 6.37337C24.0254 8.04796 23.4175 9.67311 22.2891 10.9111C22.2783 10.9229 22.2673 10.9345 22.2559 10.9458L18.721 14.4682C18.7205 14.4687 18.7201 14.4691 18.7197 14.4696C18.1172 15.0739 17.4014 15.5535 16.6133 15.8808C15.8246 16.2084 14.979 16.377 14.125 16.377C13.2711 16.377 12.4255 16.2084 11.6368 15.8808C10.8481 15.5533 10.1319 15.0732 9.52912 14.4682C9.13933 14.077 9.14051 13.4438 9.53177 13.054C9.92302 12.6642 10.5562 12.6654 10.946 13.0567C11.3629 13.4752 11.8583 13.8072 12.4039 14.0338C12.9494 14.2603 13.5343 14.377 14.125 14.377C14.7158 14.377 15.3007 14.2603 15.8462 14.0338C16.3918 13.8072 16.8872 13.4752 17.3041 13.0567L17.3067 13.0541L20.8263 9.54692C21.5983 8.69126 22.0139 7.57237 21.9872 6.41966C21.9603 5.25933 21.4874 4.15397 20.6667 3.33328C19.846 2.51259 18.7407 2.03967 17.5804 2.01281C16.4274 1.98611 15.3082 2.40189 14.4525 3.17426Z" fill="#000022"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.87508 9.62296C9.28437 9.62296 8.69946 9.73959 8.15392 9.96616C7.60837 10.1927 7.11293 10.5248 6.69601 10.9433L6.69344 10.9458L3.17381 14.453C2.40181 15.3087 1.98624 16.4276 2.01293 17.5803C2.03979 18.7406 2.51271 19.846 3.3334 20.6667C4.15409 21.4874 5.25945 21.9603 6.41978 21.9871C7.57277 22.0138 8.69193 21.598 9.54766 20.8257L12.0055 18.3679C12.396 17.9773 13.0292 17.9773 13.4197 18.3679C13.8102 18.7584 13.8102 19.3916 13.4197 19.7821L10.9447 22.2571C10.9338 22.268 10.9226 22.2786 10.9112 22.289C9.67323 23.4174 8.04809 24.0254 6.37349 23.9866C4.69889 23.9478 3.10362 23.2653 1.91919 22.0809C0.734752 20.8964 0.0522339 19.3012 0.0134652 17.6266C-0.0253035 15.952 0.582658 14.3268 1.71101 13.0888C1.72181 13.077 1.73288 13.0654 1.74423 13.0541L5.27916 9.53169C5.27961 9.53124 5.28006 9.53079 5.28051 9.53033C5.88296 8.92598 6.59873 8.44641 7.38682 8.11911C8.1755 7.79157 9.02109 7.62296 9.87508 7.62296C10.7291 7.62296 11.5747 7.79157 12.3633 8.11911C13.152 8.44666 13.8683 8.9267 14.471 9.53169C14.8608 9.92295 14.8596 10.5561 14.4684 10.9459C14.0771 11.3357 13.444 11.3345 13.0542 10.9433C12.6372 10.5248 12.1418 10.1927 11.5963 9.96616C11.0507 9.73959 10.4658 9.62296 9.87508 9.62296Z" fill="#000022"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

5
src/icons/question.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 5C9.92487 5 5 9.92487 5 16C5 22.0751 9.92487 27 16 27C22.0751 27 27 22.0751 27 16C27 9.92487 22.0751 5 16 5ZM3 16C3 8.8203 8.8203 3 16 3C23.1797 3 29 8.8203 29 16C29 23.1797 23.1797 29 16 29C8.8203 29 3 23.1797 3 16Z" fill="currentColor"/>
<path d="M16 24C16.8284 24 17.5 23.3284 17.5 22.5C17.5 21.6716 16.8284 21 16 21C15.1716 21 14.5 21.6716 14.5 22.5C14.5 23.3284 15.1716 24 16 24Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2779 9.34254C15.1002 9.00195 16.005 8.91283 16.8779 9.08647C17.7508 9.2601 18.5526 9.68869 19.182 10.318C19.8113 10.9474 20.2399 11.7492 20.4135 12.6221C20.5872 13.495 20.4981 14.3998 20.1575 15.2221C19.8169 16.0443 19.2401 16.7471 18.5001 17.2416C18.0413 17.5482 17.5325 17.7661 17 17.8875V18C17 18.5523 16.5523 19 16 19C15.4477 19 15 18.5523 15 18V17C15 16.7348 15.1054 16.4804 15.2929 16.2929C15.4804 16.1054 15.7348 16 16 16C16.4945 16 16.9778 15.8534 17.3889 15.5787C17.8 15.304 18.1205 14.9135 18.3097 14.4567C18.4989 13.9999 18.5484 13.4972 18.452 13.0123C18.3555 12.5273 18.1174 12.0819 17.7678 11.7322C17.4181 11.3826 16.9727 11.1445 16.4877 11.048C16.0028 10.9516 15.5001 11.0011 15.0433 11.1903C14.5865 11.3795 14.196 11.7 13.9213 12.1111C13.6466 12.5222 13.5 13.0055 13.5 13.5C13.5 14.0523 13.0523 14.5 12.5 14.5C11.9477 14.5 11.5 14.0523 11.5 13.5C11.5 12.61 11.7639 11.74 12.2584 10.9999C12.7529 10.2599 13.4557 9.68314 14.2779 9.34254Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -3,8 +3,10 @@ import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd'
import { Theme } from './theme'
import { ThemeProvider } from './theme/ThemeProvider'
import { BrowserRouter } from "react-router-dom";
import App from './App';
import 'typeface-lato'
import 'typeface-roboto'
import 'antd/dist/reset.css';
import './styles/reset.css';
@ -13,10 +15,12 @@ const root = ReactDOM.createRoot(
);
root.render(
<React.StrictMode>
<ConfigProvider theme={Theme}>
<ThemeProvider>
<App />
</ThemeProvider>
</ConfigProvider>
<BrowserRouter>
<ConfigProvider theme={Theme}>
<ThemeProvider>
<App />
</ThemeProvider>
</ConfigProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@ -0,0 +1,14 @@
import { observer } from 'mobx-react-lite'
import React, { FC } from 'react'
import { ActivationStore } from 'stores/ActivationStore'
import { ActivateStarter } from './pages/activate'
import { Creation } from './pages/creation'
export const Activate: FC = observer(() => {
const { code } = ActivationStore.instance()
return (
<>
{!code ? <ActivateStarter /> : <Creation />}
</>
)
})

View File

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

View File

@ -0,0 +1,56 @@
import React, { FC } from 'react'
import styled from 'styled-components'
import { Modal, ModalProps } from 'components/Modal'
import { Title, Text, Paragraph } from 'components/Typography'
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: 400px;
padding-top: 16px;
}
${props => props.theme.breakpoints.down('s')} {
padding-top: 33px;
padding-left: 4px;
padding-right: 4px;
}
`
const StyledTitle = styled(Title)`
margin-bottom: 3px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 27px !important;
}
`
export const ActivateGuide: FC<Props> = ({ onClose, ...props }) => {
const handleClose = () => {
onClose && onClose()
}
return (
<Modal {...props} width={688} destroyOnClose onCancel={handleClose}>
<StyledWrapper>
<StyledMain>
<StyledTitle>Cant find your <Text inherit primary>activation code</Text>?</StyledTitle>
<Paragraph level={2}>The activation code is located on the Kit ID Card in your home collection
kit.</Paragraph>
</StyledMain>
</StyledWrapper>
</Modal>
)
}

View File

@ -0,0 +1,95 @@
import React, { FC, useState } from 'react'
import {
StyledContainer,
StyledImageWrapper,
StyledMainContent,
StyledHeadline,
StyledContent,
StyledButton,
StyledParagraph, StyledHint, StyledFormContent,
} from './styled'
import { Form } from 'antd'
import { Paragraph, Link, Text } from 'components/Typography'
import { Input, ErrorMessage } from 'components/FormControl'
import { ActivateGuide } from './ActivateGuide'
import { observer } from 'mobx-react-lite'
import { AuthStore } from 'stores/AuthStore'
import { ActivationStore } from 'stores/ActivationStore'
import { Paths } from 'routes/Paths'
export const ActivateStarter: FC = observer(() => {
const { logined, userName } = AuthStore.instance()
const [activationStore] = useState(ActivationStore.instance())
const [showModal, setModal] = useState(false)
const handleShowModal = () => {
setModal(true)
}
const handleCloseModal = () => {
setModal(false)
}
const onFinish = (values: any) => {
// TODO: active code
console.log('Success:', values);
activationStore.setCode(values.code)
activationStore.setShouldCreateAccount(!logined)
if (logined) {
activationStore.handleNext()
}
};
return (
<StyledContainer>
<StyledImageWrapper />
<StyledMainContent>
<StyledContent>
{logined ? (
<StyledHeadline level={0} $login={logined}>
Hello, <Text inherit primary>{userName}</Text>
<br />
Welcome to your kit activation.
</StyledHeadline>
) : (
<StyledHeadline level={0} $login={logined}>
Welcome to your kit <Text inherit primary>activation</Text>.
</StyledHeadline>
)}
<StyledParagraph>Your kit needs to be activated before the lab can process your sample.</StyledParagraph>
<StyledFormContent>
<Form
layout="vertical"
onFinish={onFinish}
>
<Form.Item
label="Activation Code"
name="code"
rules={[
{
required: true, message: <ErrorMessage message="Please input your activation code" />
},
// Activation code not found
]}
>
<Input placeholder="xxxx-xxxx-xxxx" />
</Form.Item>
<Form.Item>
<StyledButton block htmlType="submit">Next</StyledButton>
</Form.Item>
</Form>
<StyledHint>
<Paragraph>
{!logined ?
<>
Already have an account?&nbsp;
<Link href={Paths.Login}>Log in</Link>
<br />
</> : null}
<Link onClick={handleShowModal}>Cant find your activation code?</Link>
</Paragraph>
</StyledHint>
</StyledFormContent>
</StyledContent>
</StyledMainContent>
<ActivateGuide open={showModal} onClose={handleCloseModal} />
</StyledContainer>
)
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

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

View File

@ -0,0 +1,83 @@
import styled from 'styled-components'
import Image from './images/activate@2x.png'
import ImageMobile from './images/activateMobile@2x.png'
import { Title, Paragraph } 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: 12px 16px;
flex-direction: column;
align-items: center;
}
`
export const StyledImageWrapper = styled.div`
width: 54%;
max-width: 737px;
height: 809px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-image: url(${Image});
flex: 0 0 auto;
${props => props.theme.breakpoints.down('s')} {
background-image: url(${ImageMobile});
max-width: 358px;
width: 100%;
height: 232px;
}
`
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: 20px;
}
`
export const StyledHeadline = styled(Title)<{$login: boolean}>`
margin-bottom: ${({ $login }) => $login ? '0' : '37'}px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 1px !important;
}
`
export const StyledParagraph = styled(Paragraph)`
margin-top: 0 !important;
margin-bottom: 0 !important;
`
export const StyledContent = styled.div`
width: 388px;
${props => props.theme.breakpoints.down('s')} {
width: 100%;
}
`
export const StyledFormContent = styled.div`
margin-top: 26px;
`
export const StyledButton = styled(Button)`
margin-top: 15px;
`
export const StyledHint = styled.div`
text-align: center;
margin-top: 33px;
${props => props.theme.breakpoints.down('s')} {
margin-top: 18px;
}
`

View File

@ -0,0 +1,147 @@
import React, { FC, useState } from 'react'
import { Form, Col, Row, Divider } from 'antd'
import { observer } from 'mobx-react-lite'
import { Link, Paragraph, Title } from 'components/Typography'
import { ErrorMessage, Input, LabelWithTooltip, Password } from 'components/FormControl'
import { StyledAccountForm, StyledAccountWrap, StyledHelp, StyledSubmitButton } from './styled'
import { ActivationStore } from 'stores/ActivationStore'
import { VerifyModal } from './VerifyModal'
import styled from 'styled-components'
import { Breakpoint } from 'components/Breakpoint'
import { ExternalPaths, Paths } from 'routes/Paths'
const StyledTitle = styled(Title)`
${props => props.theme.breakpoints.up('s')} {
margin-bottom: 6px !important;
}
`
export const Account: FC = observer(() => {
const [store] = useState(ActivationStore.instance())
const [showModal, setShowModal] = useState(false)
const handleFinish = (values: any) => {
// TODO: verify email
console.log(values)
setShowModal(true)
}
const handleClose = (success: boolean) => {
setShowModal(false)
if (success) {
// TODO: register account
store.handleNext()
}
}
return (
<StyledAccountWrap>
<StyledTitle level={2}>Create an account to activate your kit</StyledTitle>
<Paragraph style={{ marginBottom: 0 }}>If you already have an account, <Link inherit href={Paths.Login}>log
in</Link> to continue.</Paragraph>
<StyledAccountForm layout="vertical" onFinish={handleFinish}>
<Row gutter={12}>
<Col span={12}>
<Form.Item name="firstName" label="First Name"
rules={[
{
required: true, message: <ErrorMessage message="Please input your first name" />
},
]}
>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="lastName" label="Last Name" rules={[
{
required: true, message: <ErrorMessage message="Please input your last name" />
},
]}>
<Input />
</Form.Item>
</Col>
</Row>
<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="Confirm Email"
name="repeatEmail"
dependencies={['email']}
rules={[
{
required: true, message: <ErrorMessage message="Please input your confirm email" />
},
{ type: 'email', message: <ErrorMessage message="Invalid email" /> },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('email') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('The two email that you entered do not match'));
},
}),
]}
>
<Input placeholder="email@example.com" />
</Form.Item>
<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'));
},
}),
]}
extra={<StyledHelp>By clicking "Next" you acknowledge the <Link inherit href={ExternalPaths.PrivacyPolicy} target="_blank" rel="noopener noreferrer">Privacy Policy</Link> and agree to
our <Link inherit href={ExternalPaths.TermsOfUse} target="_blank" rel="noopener noreferrer">Terms of Use</Link>.</StyledHelp>}
>
<Password placeholder="********" />
</Form.Item>
<Form.Item style={{ textAlign: 'center', marginTop: 28 }}>
<StyledSubmitButton htmlType="submit">Next</StyledSubmitButton>
</Form.Item>
</StyledAccountForm>
<Breakpoint s up>
<div style={{ textAlign: "center", marginTop: 30 }}>
<Link style={{ padding: '0 17px' }} href={ExternalPaths.Disclaimer} target="_blank" rel="noopener noreferrer">Disclaimer</Link>
<Divider type="vertical" />
<Link style={{ padding: '0 17px' }} href={ExternalPaths.PrivacyPolicy} target="_blank" rel="noopener noreferrer">Privacy</Link>
</div>
</Breakpoint>
<VerifyModal open={showModal} onClose={handleClose} />
</StyledAccountWrap>
)
})

View File

@ -0,0 +1,44 @@
import React, { ChangeEvent, FC } from 'react'
import { data } from './caData'
import { SelectProps, Select, Input } from 'components/FormControl'
import { Col, Row } from 'antd'
export const CountrySelect: FC<SelectProps> = ({ style, ...props }) => {
return (
<Select
placeholder="CA"
{...props}
style={{ width: 93, ...style }}
options={data.map(label => ({ label, value: label }))}
dropdownStyle={{ width: 350 }}
dropdownMatchSelectWidth={350}
/>
)
}
type CityCountryValue = { city?: string, country: string | null }
export interface CityCountrySelectProps {
value?: CityCountryValue
onChange?: (value: CityCountryValue) => void
}
export const CityCountrySelect: FC<CityCountrySelectProps> = ({ value = { city: '', country: null }, onChange }) => {
const handleChangeCity = (e: ChangeEvent<HTMLInputElement>) => {
onChange && onChange({ ...value, city: e.target.value })
}
const handleChangeCountry = (country: string) => {
onChange && onChange({ ...value, country })
}
return (
<Row gutter={22}>
<Col flex="auto">
<Input value={value.city} onChange={handleChangeCity} />
</Col>
<Col flex="none">
<CountrySelect value={value.country} onChange={handleChangeCountry} />
</Col>
</Row>
)
}

View File

@ -0,0 +1,34 @@
import React, { FC } from 'react'
import {
StyledContainer, StyledMain, StyledParagraph, StyledStepItemContainer, StyledSteps,
} from './styled'
import { observer } from 'mobx-react-lite'
import { ActivationStore } from 'stores/ActivationStore'
import { Account } from './Account'
import { TesterInfo } from './TesterInfo'
import { Done } from './Done'
import { Review } from './Review'
export const Creation: FC = observer(() => {
const { code, step, stepItems } = ActivationStore.instance()
return (
<StyledContainer>
<StyledMain>
<StyledSteps
current={step}
barStyle={{ marginBottom: 22 }}
items={stepItems} />
<StyledStepItemContainer>
{step !== 3 ?
<StyledParagraph>You are activating your iHealth CheckMeSafe Home Collection Kit Critical 2 (HIV &
Syphilis),
ID {code}</StyledParagraph> : null}
{step === 0 ? <Account /> : null}
{step === 1 ? <TesterInfo /> : null}
{step === 2 ? <Review /> : null}
{step === 3 ? <Done /> : null}
</StyledStepItemContainer>
</StyledMain>
</StyledContainer>
)
})

View File

@ -0,0 +1,73 @@
import React, { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Link, Text, Title } from 'components/Typography'
import { ActivationStore } from 'stores/ActivationStore'
import styled from 'styled-components'
import { ReactComponent as SuccessSvg } from './images/success.svg'
import { Paths, ExternalPaths } from '../../../../../routes/Paths'
const StyledLinkItem = styled.div`
margin-bottom: 6px;
&:first-child {
margin-top: 12px;
}
`
const StyledImageWrapper = styled.div`
position: relative;
height: 231px;
text-align: center;
${props => props.theme.breakpoints.up('s')} {
height: 316px;
text-align: left;
padding-left: 103px;
}
> svg {
width: 223px;
height: 231px;
${props => props.theme.breakpoints.up('s')} {
width: 301px;
height: 385px;
position: absolute;
top: -15px;
}
}
`
const StyledTitle = styled(Title)`
margin-bottom: 7px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 4px !important;
}
`
export const Done: FC = observer(() => {
const [store] = useState(ActivationStore.instance())
const { code } = store
return (
<>
<StyledTitle level={0}>Your kit is <Text primary inherit>activated!</Text></StyledTitle>
<Title level={2}>You have just activated your iHealth CheckmeSafe Home Collection Kit Critical 2 - HIV & Syphilis
ID {code}</Title>
<StyledImageWrapper>
<SuccessSvg />
</StyledImageWrapper>
<Title level={2}>Whats Next?</Title>
<div>
<StyledLinkItem>
<Link href={ExternalPaths.GuideVideo} target="_blank" rel="noopener noreferrer">Watch a video on how to
collect your sample</Link>
</StyledLinkItem>
<StyledLinkItem>
<Link href={Paths.Dashboard}>Go to my dashboard</Link>
</StyledLinkItem>
<StyledLinkItem>
<Link href={ExternalPaths.Faqs} target="_blank" rel="noopener noreferrer">View FAQs</Link>
</StyledLinkItem>
</div>
</>
)
})

View File

@ -0,0 +1,24 @@
import React, { FC } from 'react'
import { Select, SelectProps } from 'components/FormControl'
export const EthnicitySelect: FC<SelectProps> = (props) => {
return (
<Select {...props}
options={[
{
"label": "Hispanic/Latino",
"value": "Hispanic/Latino"
},
{
"label": "Non-Hispanic/non-Latino",
"value": "Non-Hispanic/non-Latino"
},
{
"label": "Unknown",
"value": "Unknown"
}
]}
/>
)
}

View File

@ -0,0 +1,36 @@
import React, { FC } from 'react'
import { Select, SelectProps } from 'components/FormControl'
export const RaceSelect: FC<SelectProps> = (props) => {
return (
<Select {...props}
options={[
{
"label": "African-American/Black",
"value": "African-American/Black"
},
{
"label": "American Indian/Alaska Native",
"value": "American Indian/Alaska Native"
},
{
"label": "Asian",
"value": "Asian"
},
{
"label": "Pacific Islander",
"value": "Pacific Islander"
},
{
"label": "White",
"value": "White"
},
{
"label": "Other/Unknown",
"value": "Other/Unknown"
}
]}
/>
)
}

View File

@ -0,0 +1,100 @@
import React, { FC, PropsWithChildren, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Paragraph, Text, Title } from 'components/Typography'
import { AuthStore } from 'stores/AuthStore'
import { Col, Row } from 'antd'
import styled from 'styled-components'
import { ActivationStore } from 'stores/ActivationStore'
import { Button } from 'components/Button'
import Icon from '@ant-design/icons'
import { ReactComponent as CaretLeft } from 'icons/caretLeft.svg'
const StyledTitle = styled(Title)`
margin-bottom: 11px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 4px !important;
}
`
const StyledParagraph = styled(Paragraph)`
margin-bottom: 30px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 28px !important;
}
`
const StyledRow = styled(Row)`
margin-top: 147px;
${props => props.theme.breakpoints.down('s')} {
justify-content: center;
margin-top: 36px;
}
`
const StyleButton = styled(Button)`
width: 358px;
${props => props.theme.breakpoints.down('s')} {
width: 150px;
}
`
const StyledItem = styled.div`
font-size: 16px;
margin-bottom: 10px;
`
const StyledLabel = styled.div`
font-weight: 700;
margin-bottom: 4px;
line-height: 18px
`
const StyledValue = styled(Paragraph)``
const ReviewItem: FC<PropsWithChildren<{ label: string }>> = ({ label, children }) => {
return (
<StyledItem>
<StyledLabel>{label}</StyledLabel>
<StyledValue>{children}</StyledValue>
</StyledItem>
)
}
export const Review: FC = observer(() => {
const { userName, userInfo } = AuthStore.instance()
const [store] = useState(ActivationStore.instance())
const { testerInfo } = store
return (
<>
<StyledTitle level={0}>Hello, <Text primary inherit>{userName}</Text></StyledTitle>
<StyledParagraph style={{ marginBottom: 30 }}>Please review the testers information below.</StyledParagraph>
<Row gutter={22}>
<Col flex="none">
<ReviewItem label="First Name">{testerInfo.firstName}</ReviewItem>
</Col>
<Col flex="none">
<ReviewItem label="Last Name">{testerInfo.lastName}</ReviewItem>
</Col>
</Row>
<ReviewItem label="Sex assigned at birth?">{testerInfo.gender === 'female' ? 'Female' : 'Male'}</ReviewItem>
<ReviewItem label="Race">{testerInfo.race}</ReviewItem>
<ReviewItem label="Ethnicity">{testerInfo.ethnicity}</ReviewItem>
<ReviewItem label="Phone Number">{testerInfo.phone}</ReviewItem>
<ReviewItem
label="Address">{[testerInfo.address, testerInfo.address2, testerInfo.city + ', ' + testerInfo.ca, testerInfo.postal].filter(Boolean).map((text, index) => (
<div key={index}>{text}</div>
))}</ReviewItem>
<ReviewItem label="Email(s) to be notified">
<div>{testerInfo.email || userInfo?.email}</div>
{(testerInfo.additionalEmails || []).map((e: string, index: number) => <div key={index}>{e}</div>)}
</ReviewItem>
<StyledRow align="middle" gutter={18} wrap={false} >
<Col flex="none">
<Button icon={<Icon component={CaretLeft} />} onClick={store.handlePrev.bind(store)} />
</Col>
<Col flex="none">
<StyleButton onClick={store.handleNext.bind(store)}>Submit</StyleButton>
</Col>
</StyledRow>
</>
)
})

View File

@ -0,0 +1,298 @@
import React, { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Paragraph, Text, Title } from 'components/Typography'
import { Col, Form, Row, Space } from 'antd'
import { Button } from 'components/Button'
import { AuthStore } from 'stores/AuthStore'
import { ErrorMessage, Input, Radio, RadioGroup, Checkbox } from 'components/FormControl'
import { RadioChangeEvent } from 'antd/es/radio/interface'
import styled from 'styled-components'
import { ReactComponent as CaretLeft } from 'icons/caretLeft.svg'
import Icon, { PlusOutlined, MinusOutlined } from '@ant-design/icons'
import { RaceSelect } from './RaceSelect'
import { EthnicitySelect } from './EthnicitySelect'
import { CountrySelect } from './CityCountrySelect'
import { data } from './caData'
import { ActivationStore } from 'stores/ActivationStore'
import { StyledSubmitButton } from './styled'
const StyledRadio = styled(Radio)`
${props => props.theme.breakpoints.down('s')} {
.ant-radio-checked .ant-radio-inner::after {
background: ${props => props.theme.brandPrimary};
}
}
`
const StyledRadioDiv = styled.div`
display: inline-block;
padding: 0 20px;
width: 122px;
`
const StyledTitle = styled(Title)`
margin-bottom: 28px !important;
${props => props.theme.breakpoints.down('s')} {
margin-top: 13px;
margin-bottom: 12px !important;
}
`
const StyledButtonWrapper = styled.div`
text-align: center;
margin-top: 80px;
${props => props.theme.breakpoints.down('s')} {
margin-top: 30px;
}
`
const StyledFormTitle = styled(Title)`
margin-bottom: 11px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 6px !important;
}
`
const StyledForm = styled(Form)`
margin-top: 23px;
${props => props.theme.breakpoints.down('s')} {
margin-top: 13px;
}
`
const StyledFormButtonWrapper = styled(Form.Item)`
margin-top: 65px;
${props => props.theme.breakpoints.down('s')} {
margin-top: 136px;
.ant-row {
justify-content: center;
}
}
`
const StyleFormButton = styled(Button)`
width: 358px;
${props => props.theme.breakpoints.down('s')} {
width: 150px;
}
`
export const TesterInfo: FC = observer(() => {
const [form] = Form.useForm()
const firstName = Form.useWatch('firstName', form)
const lastName = Form.useWatch('lastName', form)
const { userName, userInfo } = AuthStore.instance()
const [store] = useState(ActivationStore.instance())
const [forMe, setForMe] = useState(1)
const [innerStep, setInnerStep] = useState(0)
const handleForMe = (e: RadioChangeEvent) => {
setForMe(e.target.value)
}
const handleInnerNext = () => {
setInnerStep(1)
}
const handleInnerPrev = () => {
setInnerStep(0)
form.resetFields()
}
const handleFinish = (values: any) => {
console.log(values)
// TODO: setup values
store.setTesterInfo(values)
store.handleNext()
}
return (
<div>
{innerStep === 0 ? (
<>
<StyledTitle level={2}>You have successfully verified your email address.</StyledTitle>
<Paragraph style={{ marginBottom: 30 }}>Who will be taking the test?</Paragraph>
<RadioGroup value={forMe} onChange={handleForMe}>
<Space direction="vertical">
<StyledRadio value={1}>I, {userName}, will take the
test.</StyledRadio>
<StyledRadio value={0}>This test is for someone else. </StyledRadio>
</Space>
</RadioGroup>
<StyledButtonWrapper>
<StyledSubmitButton onClick={handleInnerNext}>Next</StyledSubmitButton>
</StyledButtonWrapper>
</>
) : (
<>
<StyledFormTitle level={0}>Hello, <Text primary inherit>{userName}</Text></StyledFormTitle>
<Paragraph>Please provide information of <b>the person being tested</b>.</Paragraph>
<StyledForm layout="vertical"
onFinish={handleFinish} form={form}
initialValues={{
gender: 'female',
firstName: userInfo?.firstName,
lastName: userInfo?.lastName,
additionalEmails: [""],
ca: data[0]
}}>
<Row gutter={12}>
<Col span={12}>
<Form.Item label="First Name" name="firstName"
rules={[
{
required: true, message: <ErrorMessage message="Please input your first name" />
},
]}>
<Input readOnly={Boolean(forMe)} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="lastName" label="Last Name" rules={[
{
required: true, message: <ErrorMessage message="Please input your last name" />
},
]}>
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item name="gender" label={<div>
<Paragraph>Sex assigned at birth?</Paragraph>
<Paragraph italic style={{ fontFamily: 'Roboto' }}>
We need to know your sex at birth as the lab requires this information to provide the correct reference
range for your results.</Paragraph>
</div>}
rules={[
{
required: true, message: <ErrorMessage message="Please select your gender" />
},
]}
>
<RadioGroup>
<Radio value="female"><StyledRadioDiv>Female</StyledRadioDiv></Radio>
<Radio value="male"><StyledRadioDiv>Male</StyledRadioDiv></Radio>
</RadioGroup>
</Form.Item>
<Form.Item label="Race" name="race" rules={[
{
required: true, message: <ErrorMessage message="Please select your race" />
},
]}>
<RaceSelect />
</Form.Item>
<Form.Item label="Ethnicity" name="ethnicity"
rules={[
{
required: true, message: <ErrorMessage message="Please input your ethnicity" />
},
]}
>
<EthnicitySelect />
</Form.Item>
<Form.Item label="Phone Number" name="phone"
rules={[
{
required: true, message: <ErrorMessage message="Please input your phone number" />
},
]}
>
<Input placeholder="(xxx) - xxx - xxxx" />
</Form.Item>
<Form.Item label="Address" name="address" rules={[
{
required: true, message: <ErrorMessage message="Please input your address" />
},
]}>
<Input />
</Form.Item>
<Form.Item label="Address line2" name="address2"><Input /></Form.Item>
<Row gutter={22}>
<Col flex="auto">
<Form.Item label="City" name="city" rules={[
{
required: true, message: <ErrorMessage message="Please input your city" />
},
]}>
<Input />
</Form.Item>
</Col>
<Col flex="none">
<Form.Item name="ca" label=" ">
<CountrySelect />
</Form.Item>
</Col>
</Row>
<Form.Item label="Postal / Zip Code" name="postal" rules={[
{
required: true, message: <ErrorMessage message="Please input your postal/zip code" />
},
]}><Input /></Form.Item>
<Form.List name="additionalEmails">
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Row gutter={22} align="bottom" wrap={false}>
<Col flex="auto">
<Form.Item {...field}
rules={[
{ type: 'email', message: <ErrorMessage message="Invalid email" /> },
]}
label={index === 0 ? "Send results notifications to additional emails. (Optional)" : ""}
key={field.key}>
<Input />
</Form.Item>
</Col>
<Col flex="none">
<Form.Item label={index === 0 ? " " : ""}>
{index === fields.length - 1 &&
<Button
onClick={() => add()}
icon={<PlusOutlined />}
/>}
{index < fields.length - 1 &&
<Button
onClick={() => remove(index)}
icon={<MinusOutlined />}
/>}
</Form.Item>
</Col>
</Row>
))}
<Form.ErrorList errors={errors} />
</>
)}
</Form.List>
{!forMe &&
<Form.Item name="checked" valuePropName="checked" rules={[{
required: true,
validateTrigger: 'onSubmit',
transform: value => value ? 'y' : '',
message: <ErrorMessage message="Please accept agreement" />
}]}>
<Checkbox>
By checking this box, I indicate that I {userName}, have
obtained {firstName || lastName ? [firstName, lastName].filter(Boolean).join(' ') : '[Tester\'s Name]'}'s
consent
to assist him/her in the sample collection process and fill out any necessary forms on his/her behalf.
</Checkbox>
</Form.Item>
}
<StyledFormButtonWrapper>
<Row align="middle" gutter={18} wrap={false}>
<Col flex="none">
<Button icon={<Icon component={CaretLeft} />} onClick={handleInnerPrev} />
</Col>
<Col flex="none">
<StyleFormButton htmlType="submit">Next</StyleFormButton>
</Col>
</Row>
</StyledFormButtonWrapper>
</StyledForm>
</>
)}
</div>
)
})

View File

@ -0,0 +1,141 @@
import React, { FC } from 'react'
import styled from 'styled-components'
import { Captcha, ErrorMessage } from 'components/FormControl'
import { Modal, ModalProps } from 'components/Modal'
import { Title, Paragraph, Text } from 'components/Typography'
import { ReactComponent as EmailSvg } from './images/email.svg'
import { ReactComponent as EmailWarningSvg } from './images/emailWarning.svg'
import { Form } from 'antd'
import { Button } from 'components/Button'
import { Breakpoint } from 'components/Breakpoint'
import { observer } from 'mobx-react-lite'
import { AuthStore } from 'stores/AuthStore'
import { Paths } from 'routes/Paths'
type Props = Pick<ModalProps, 'open'> & {
onClose?: (success: boolean) => void
}
const StyledContent = styled.div`
display: flex;
flex-direction: row;
height: 366px;
justify-content: center;
align-items: center;
`
const StyledImageWrapper = styled.div`
flex: 0 0 250px;
> svg {
width: 250px;
height: 235px;
}
`
const StyledFormWrapper = styled.div`
flex: 1;
padding-left: 44px;
${props => props.theme.breakpoints.down('s')} {
padding-left: 5px;
}
`
const StyledFormTitle = styled(Title)`
${props => props.theme.breakpoints.up('s')} {
margin-bottom: 4px !important;
}
`
const StyledCodeFormItem = styled(Form.Item)`
padding: 28px 0;
${props => props.theme.breakpoints.down('s')} {
padding: 40px 0;
}
`
const StyledRegisterTitle = styled(Title)`
${props => props.theme.breakpoints.up('s')} {
margin-bottom: 28px !important;
}
`
const StyledParagraph = styled(Paragraph)`
margin-bottom: 53px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 9px !important;
}
`
const StyledRegisterImageWrapper = styled.div`
text-align: center;
margin-bottom: 19px;
> svg {
width: 188px;
height: 176px;
}
`
export const VerifyModal: FC<Props> = observer(({ onClose, ...props }) => {
const { userInfo } = AuthStore.instance()
const [expried, setExpried] = React.useState('9:45')
const [hasRegistered, setHasRegistered] = React.useState(false)
const handleSendCode = () => {}
const handleClose = (success: boolean) => () => {
onClose && onClose(success)
}
const handleFinish = ({ code }: { code: string }) => {
console.log(code)
// TODO: 验证
handleClose(true)()
}
return (
<Modal {...props} onCancel={handleClose(false)} width={688}>
<StyledContent>
<Breakpoint s up>
<StyledImageWrapper>
<EmailSvg />
</StyledImageWrapper>
</Breakpoint>
<StyledFormWrapper>
{hasRegistered ? (
<>
<Breakpoint s down>
<StyledRegisterImageWrapper>
<EmailWarningSvg />
</StyledRegisterImageWrapper>
</Breakpoint>
<StyledRegisterTitle level={2}>Email address is already registered</StyledRegisterTitle>
<StyledParagraph>Please login to your existing account.</StyledParagraph>
<Button htmlType="submit" block href={Paths.Login}>Log in</Button>
</>
) : (
<>
<StyledFormTitle level={2}>Verify your email</StyledFormTitle>
<Paragraph>A 6-digit code has been sent to {userInfo?.email}</Paragraph>
<Form layout="vertical" onFinish={handleFinish}>
<StyledCodeFormItem name="code" rules={[
{
required: true, message: <ErrorMessage message="Please input your verification code" />
},
{
len: 6,
validateTrigger: 'onSubmit',
message: <ErrorMessage message="The verification code you entered is invalid." />
},
]}>
<Captcha />
</StyledCodeFormItem>
<Paragraph>The code will expire in {expried}</Paragraph>
<Paragraph>Didnt receive the code? <Text primary inherit onClick={handleSendCode}>Send
again</Text></Paragraph>
<Form.Item style={{ marginTop: 16 }}>
<Button htmlType="submit" block>Verify</Button>
</Form.Item>
</Form></>)}
</StyledFormWrapper>
</StyledContent>
</Modal>
)
})

View File

@ -0,0 +1,240 @@
export const data = [
"United States",
"Afghanistan",
"Åland Islands",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Anguilla",
"Antigua & Barbuda",
"Argentina",
"Armenia",
"Aruba",
"Ascension Island",
"Australia",
"Austria",
"Azerbaijan",
"Bahamas",
"Bahrain",
"Bangladesh",
"Barbados",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bermuda",
"Bhutan",
"Bolivia",
"Bosnia & Herzegovina",
"Botswana",
"Brazil",
"British Indian Ocean Territory",
"British Virgin Islands",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Cape Verde",
"Caribbean Netherlands",
"Cayman Islands",
"Central African Republic",
"Chad",
"Chile",
"China",
"Christmas Island",
"Cocos (Keeling) Islands",
"Colombia",
"Comoros",
"Congo - Brazzaville",
"Congo - Kinshasa",
"Cook Islands",
"Costa Rica",
"Croatia",
"Curaçao",
"Cyprus",
"Czechia",
"Côte dIvoire",
"Denmark",
"Djibouti",
"Dominica",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Equatorial Guinea",
"Eritrea",
"Estonia",
"Eswatini",
"Ethiopia",
"Falkland Islands",
"Faroe Islands",
"Fiji",
"Finland",
"France",
"French Guiana",
"French Polynesia",
"French Southern Territories",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Gibraltar",
"Greece",
"Greenland",
"Grenada",
"Guadeloupe",
"Guatemala",
"Guernsey",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Hong Kong SAR",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iraq",
"Ireland",
"Isle of Man",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jersey",
"Jordan",
"Kazakhstan",
"Kenya",
"Kiribati",
"Kosovo",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Latvia",
"Lebanon",
"Lesotho",
"Liberia",
"Libya",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Macao SAR",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Martinique",
"Mauritania",
"Mauritius",
"Mayotte",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Montenegro",
"Montserrat",
"Morocco",
"Mozambique",
"Myanmar (Burma)",
"Namibia",
"Nauru",
"Nepal",
"Netherlands",
"New Caledonia",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"Niue",
"Norfolk Island",
"North Macedonia",
"Norway",
"Oman",
"Pakistan",
"Palestinian Territories",
"Panama",
"Papua New Guinea",
"Paraguay",
"Peru",
"Philippines",
"Pitcairn Islands",
"Poland",
"Portugal",
"Qatar",
"Réunion",
"Romania",
"Russia",
"Rwanda",
"Samoa",
"San Marino",
"São Tomé & Príncipe",
"Saudi Arabia",
"Senegal",
"Serbia",
"Seychelles",
"Sierra Leone",
"Singapore",
"Sint Maarten",
"Slovakia",
"Slovenia",
"Solomon Islands",
"Somalia",
"South Africa",
"South Georgia & South Sandwich Islands",
"South Korea",
"South Sudan",
"Spain",
"Sri Lanka",
"St. Barthélemy",
"St. Helena",
"St. Kitts & Nevis",
"St. Lucia",
"St. Martin",
"St. Pierre & Miquelon",
"St. Vincent & Grenadines",
"Sudan",
"Suriname",
"Svalbard & Jan Mayen",
"Sweden",
"Switzerland",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Timor-Leste",
"Togo",
"Tokelau",
"Tonga",
"Trinidad & Tobago",
"Tristan da Cunha",
"Tunisia",
"Turkey",
"Turkmenistan",
"Turks & Caicos Islands",
"Tuvalu",
"U.S. Outlying Islands",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"Uruguay",
"Uzbekistan",
"Vanuatu",
"Vatican City",
"Venezuela",
"Vietnam",
"Wallis & Futuna",
"Western Sahara",
"Yemen",
"Zambia",
"Zimbabwe"
]

View File

@ -0,0 +1,16 @@
<svg width="250" height="235" viewBox="0 0 250 235" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="22" y="81" width="205" height="125" rx="7" fill="#FF712E"/>
<path d="M122.988 142.829C124.232 143.553 125.768 143.553 127.012 142.829L220.439 88.4572C223.97 86.4024 222.513 81 218.428 81H31.5725C27.4874 81 26.0298 86.4024 29.5605 88.4572L122.988 142.829Z" fill="#F7F7F7"/>
<path d="M122.847 136.092C123.849 135.43 125.151 135.43 126.153 136.092L223.668 200.497C226.15 202.136 224.989 206 222.014 206H26.9858C24.0109 206 22.8501 202.136 25.3324 200.497L122.847 136.092Z" fill="#EB6E33"/>
<path d="M28 4C28 1.79086 29.7909 0 32 0H218C220.209 0 222 1.79086 222 4V86H28V4Z" fill="#F7F7F7"/>
<ellipse cx="124.5" cy="217" rx="98.5" ry="18" fill="url(#paint0_radial_493_11093)"/>
<path d="M92.1975 52.8625C91.8675 53.0642 91.5375 53.2383 91.2075 53.385C90.8958 53.5133 90.5658 53.605 90.2175 53.66C90.9325 53.77 91.5925 54.0267 92.1975 54.43L96.57 56.9875L94.975 59.7375L90.6025 57.18C90.2725 56.9783 89.9608 56.7675 89.6675 56.5475C89.3925 56.3275 89.1542 56.0892 88.9525 55.8325C89.1908 56.4375 89.31 57.1342 89.31 57.9225V62.9825H86.065V57.95C86.065 57.5833 86.0833 57.2258 86.12 56.8775C86.175 56.5292 86.2575 56.1992 86.3675 55.8875C86.1658 56.1442 85.9275 56.3733 85.6525 56.575C85.3775 56.7767 85.0842 56.9783 84.7725 57.18L80.4275 59.71L78.805 56.96L83.1775 54.43C83.8742 54.0267 84.5342 53.77 85.1575 53.66C84.5158 53.55 83.8558 53.2842 83.1775 52.8625L78.805 50.305L80.4 47.555L84.7725 50.14C85.1025 50.3417 85.405 50.5525 85.68 50.7725C85.955 50.9742 86.2025 51.2033 86.4225 51.46C86.1842 50.855 86.065 50.1675 86.065 49.3975V44.31H89.31V49.3425C89.31 49.7275 89.2825 50.0942 89.2275 50.4425C89.1908 50.7908 89.1083 51.1208 88.98 51.4325C89.2 51.1758 89.4475 50.9467 89.7225 50.745C89.9975 50.5433 90.2908 50.3417 90.6025 50.14L94.9475 47.61L96.57 50.36L92.1975 52.8625Z" fill="#8F9296"/>
<path d="M128.198 53.8625C127.868 54.0642 127.538 54.2383 127.208 54.385C126.896 54.5133 126.566 54.605 126.218 54.66C126.933 54.77 127.593 55.0267 128.198 55.43L132.57 57.9875L130.975 60.7375L126.603 58.18C126.273 57.9783 125.961 57.7675 125.668 57.5475C125.393 57.3275 125.154 57.0892 124.953 56.8325C125.191 57.4375 125.31 58.1342 125.31 58.9225V63.9825H122.065V58.95C122.065 58.5833 122.083 58.2258 122.12 57.8775C122.175 57.5292 122.258 57.1992 122.368 56.8875C122.166 57.1442 121.928 57.3733 121.653 57.575C121.378 57.7767 121.084 57.9783 120.773 58.18L116.428 60.71L114.805 57.96L119.178 55.43C119.874 55.0267 120.534 54.77 121.158 54.66C120.516 54.55 119.856 54.2842 119.178 53.8625L114.805 51.305L116.4 48.555L120.773 51.14C121.103 51.3417 121.405 51.5525 121.68 51.7725C121.955 51.9742 122.203 52.2033 122.423 52.46C122.184 51.855 122.065 51.1675 122.065 50.3975V45.31H125.31V50.3425C125.31 50.7275 125.283 51.0942 125.228 51.4425C125.191 51.7908 125.108 52.1208 124.98 52.4325C125.2 52.1758 125.448 51.9467 125.723 51.745C125.998 51.5433 126.291 51.3417 126.603 51.14L130.948 48.61L132.57 51.36L128.198 53.8625Z" fill="#8F9296"/>
<path d="M164.198 53.8625C163.868 54.0642 163.538 54.2383 163.208 54.385C162.896 54.5133 162.566 54.605 162.218 54.66C162.933 54.77 163.593 55.0267 164.198 55.43L168.57 57.9875L166.975 60.7375L162.603 58.18C162.273 57.9783 161.961 57.7675 161.668 57.5475C161.393 57.3275 161.154 57.0892 160.953 56.8325C161.191 57.4375 161.31 58.1342 161.31 58.9225V63.9825H158.065V58.95C158.065 58.5833 158.083 58.2258 158.12 57.8775C158.175 57.5292 158.258 57.1992 158.368 56.8875C158.166 57.1442 157.928 57.3733 157.653 57.575C157.378 57.7767 157.084 57.9783 156.773 58.18L152.428 60.71L150.805 57.96L155.178 55.43C155.874 55.0267 156.534 54.77 157.158 54.66C156.516 54.55 155.856 54.2842 155.178 53.8625L150.805 51.305L152.4 48.555L156.773 51.14C157.103 51.3417 157.405 51.5525 157.68 51.7725C157.955 51.9742 158.203 52.2033 158.423 52.46C158.184 51.855 158.065 51.1675 158.065 50.3975V45.31H161.31V50.3425C161.31 50.7275 161.283 51.0942 161.228 51.4425C161.191 51.7908 161.108 52.1208 160.98 52.4325C161.2 52.1758 161.448 51.9467 161.723 51.745C161.998 51.5433 162.291 51.3417 162.603 51.14L166.948 48.61L168.57 51.36L164.198 53.8625Z" fill="#8F9296"/>
<defs>
<radialGradient id="paint0_radial_493_11093" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(124.5 217) rotate(90) scale(18 98.5)">
<stop stop-color="#8F9296"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,14 @@
<svg width="188" height="177" viewBox="0 0 188 177" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="16.5439" y="60.9121" width="154.16" height="94" rx="7" fill="#FF712E"/>
<path d="M91.988 107.117C93.2317 107.84 94.7683 107.84 96.012 107.117L162.593 68.3688C166.123 66.314 164.666 60.9116 160.581 60.9116H27.4192C23.3342 60.9116 21.8766 66.314 25.4073 68.3688L91.988 107.117Z" fill="#F5F5F5"/>
<path d="M91.9706 102.612C92.9733 101.95 94.2745 101.95 95.2773 102.612L166.131 149.409C168.614 151.048 167.453 154.912 164.478 154.912H22.7697C19.7948 154.912 18.634 151.048 21.1164 149.409L91.9706 102.612Z" fill="#EB6E33"/>
<path d="M21.0562 4C21.0562 1.79086 22.847 0 25.0562 0H162.944C165.153 0 166.944 1.79086 166.944 4V64.672H21.0562V4Z" fill="#F5F5F5"/>
<ellipse cx="93.624" cy="163.184" rx="74.072" ry="13.536" fill="url(#paint0_radial_492_9893)"/>
<path d="M93.4628 24.1714C88.3634 24.1714 83.3786 25.6835 79.1386 28.5166C74.8986 31.3496 71.594 35.3764 69.6425 40.0876C67.6911 44.7988 67.1805 49.9828 68.1754 54.9842C69.1702 59.9856 71.6258 64.5797 75.2316 68.1855C78.8374 71.7913 83.4314 74.2468 88.4328 75.2417C93.4342 76.2365 98.6183 75.7259 103.329 73.7745C108.041 71.823 112.067 68.5184 114.9 64.2784C117.734 60.0385 119.246 55.0536 119.246 49.9542C119.238 43.1184 116.52 36.5646 111.686 31.731C106.852 26.8973 100.299 24.1786 93.4628 24.1714ZM93.4628 71.7705C89.1479 71.7705 84.93 70.491 81.3423 68.0938C77.7547 65.6966 74.9584 62.2894 73.3072 58.303C71.656 54.3166 71.2239 49.93 72.0657 45.6981C72.9075 41.4662 74.9853 37.5789 78.0364 34.5278C81.0874 31.4768 84.9747 29.399 89.2067 28.5572C93.4386 27.7154 97.8251 28.1474 101.812 29.7986C105.798 31.4499 109.205 34.2461 111.602 37.8338C114 41.4214 115.279 45.6394 115.279 49.9542C115.272 55.7383 112.972 61.2835 108.882 65.3734C104.792 69.4633 99.2468 71.7639 93.4628 71.7705ZM91.4795 51.9375V38.0545C91.4795 37.5285 91.6885 37.024 92.0604 36.6521C92.4323 36.2801 92.9368 36.0712 93.4628 36.0712C93.9888 36.0712 94.4933 36.2801 94.8652 36.6521C95.2371 37.024 95.4461 37.5285 95.4461 38.0545V51.9375C95.4461 52.4635 95.2371 52.968 94.8652 53.3399C94.4933 53.7119 93.9888 53.9208 93.4628 53.9208C92.9368 53.9208 92.4323 53.7119 92.0604 53.3399C91.6885 52.968 91.4795 52.4635 91.4795 51.9375ZM96.4377 60.8624C96.4377 61.4508 96.2633 62.0259 95.9364 62.5152C95.6095 63.0044 95.1449 63.3857 94.6013 63.6109C94.0577 63.836 93.4595 63.8949 92.8824 63.7802C92.3053 63.6654 91.7752 63.382 91.3592 62.966C90.9431 62.5499 90.6598 62.0198 90.545 61.4427C90.4302 60.8657 90.4891 60.2675 90.7143 59.7239C90.9395 59.1803 91.3208 58.7157 91.81 58.3888C92.2992 58.0619 92.8744 57.8874 93.4628 57.8874C94.2518 57.8874 95.0085 58.2009 95.5664 58.7588C96.1243 59.3167 96.4377 60.0734 96.4377 60.8624Z" fill="#FF5200"/>
<defs>
<radialGradient id="paint0_radial_492_9893" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(93.624 163.184) rotate(90) scale(13.536 74.072)">
<stop stop-color="#8F9296"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,31 @@
<svg width="301" height="385" viewBox="0 0 301 385" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M154.67 299.283C234.183 299.283 298.642 281.802 298.642 260.238C298.642 238.674 234.183 221.193 154.67 221.193C75.1559 221.193 10.6973 238.674 10.6973 260.238C10.6973 281.802 75.1559 299.283 154.67 299.283Z" fill="url(#paint0_radial_493_11687)"/>
<path d="M50.0446 136.566C50.4073 135.113 50.0446 133.66 49.138 132.571L28.4669 108.236C26.1097 105.33 21.3952 106.601 20.4886 110.234C20.1259 111.686 20.4886 113.139 21.3952 114.229L42.0663 138.382C44.4235 141.288 49.138 140.198 50.0446 136.566Z" fill="#D9D9D9"/>
<path d="M206.528 114.047C208.522 114.592 210.517 113.866 211.786 112.231L246.238 67.3749C248.414 64.6509 246.963 60.6556 243.699 59.7476C241.705 59.2028 239.71 59.9292 238.441 61.5636L203.989 106.42C201.632 109.325 203.083 113.321 206.528 114.047Z" fill="#D9D9D9"/>
<path d="M100.272 111.505C101.179 109.87 101.179 108.054 100.091 106.601L67.8149 62.2901C65.8203 59.566 61.6498 59.7476 59.8366 62.6533C58.93 64.2877 58.93 66.1038 60.0179 67.5566L92.2938 111.686C94.2884 114.41 98.4589 114.229 100.272 111.505Z" fill="#D9D9D9"/>
<path d="M247.145 147.099C248.595 147.281 250.046 146.736 250.953 145.465L271.624 120.04C273.8 117.498 272.168 113.502 268.904 113.139C267.453 112.958 266.003 113.502 265.096 114.774L244.425 140.198C242.249 142.741 243.7 146.736 247.145 147.099Z" fill="#D9D9D9"/>
<mask id="mask0_493_11687" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="100" y="0" width="103" height="102">
<path d="M202.902 0H100.453V101.153H202.902V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_493_11687)">
<path d="M173.527 47.3985C173.889 47.7617 174.071 48.125 174.252 48.4882C174.433 48.8514 174.433 49.2146 174.433 49.7594C174.433 50.1226 174.433 50.6674 174.252 51.0306C174.071 51.3938 173.889 51.757 173.527 52.1202L151.224 73.9127C150.861 74.2759 150.498 74.4575 150.136 74.6391C149.773 74.8207 149.41 74.8207 148.866 74.8207C148.504 74.8207 147.96 74.8207 147.597 74.6391C147.234 74.4575 146.872 74.2759 146.509 73.9127L136.899 64.4693C136.355 63.9245 135.992 63.0165 135.992 62.29C135.992 61.382 136.355 60.6556 136.899 60.1108C137.443 59.566 138.349 59.2028 139.075 59.2028C139.981 59.2028 140.707 59.566 141.251 60.1108L148.504 67.3749L168.631 47.3985C168.993 47.0353 169.356 46.8537 169.719 46.6721C170.081 46.4905 170.444 46.4905 170.988 46.4905C171.351 46.4905 171.895 46.4905 172.257 46.6721C172.983 46.8537 173.345 47.0353 173.527 47.3985ZM196.918 59.0212C196.918 67.1933 194.56 75.1839 189.846 81.9033C185.313 88.6226 178.785 93.8891 171.169 96.9764C163.554 100.064 155.213 100.972 147.053 99.3372C139.075 97.7028 131.64 93.8891 125.838 88.0778C120.036 82.2665 116.046 75.0023 114.415 67.0117C112.783 59.0212 113.689 50.849 116.772 43.2216C119.854 35.5943 125.294 29.2382 132.003 24.6981C138.712 20.158 146.872 17.7971 155.031 17.7971C166.092 17.7971 176.609 22.1556 184.406 29.783C192.566 37.7735 196.918 48.125 196.918 59.0212ZM190.571 59.0212C190.571 52.1202 188.577 45.4009 184.587 39.7712C180.78 33.9599 175.159 29.6014 168.812 26.8773C162.466 24.1533 155.394 23.6084 148.504 24.8797C141.613 26.1509 135.448 29.6014 130.552 34.3231C125.657 39.2264 122.212 45.4009 120.942 52.1202C119.673 58.8396 120.217 65.9221 122.937 72.2783C125.657 78.6344 130.19 84.0825 135.811 87.8962C141.613 91.7099 148.322 93.7075 155.394 93.7075C164.642 93.7075 173.708 90.0754 180.236 83.5377C186.763 77.1816 190.571 68.283 190.571 59.0212Z" fill="#FF5200"/>
</g>
<path d="M86.1302 159.993C113.692 177.245 115.686 197.04 81.5971 206.12L28.4688 175.429L86.1302 159.993Z" fill="#54565A"/>
<path d="M156.846 249.705L268.724 219.741L258.932 214.111C217.228 189.958 140.708 187.597 71.8047 200.672L156.846 249.705Z" fill="#54565A"/>
<path d="M85.7675 160.175C107.527 173.613 109.702 193.408 76.3386 203.215L28.4688 175.429L85.7675 160.175Z" fill="#FF4D00"/>
<path d="M156.846 249.705L268.905 219.559L264.554 217.017C222.849 192.863 139.983 189.413 76.3379 203.215L156.846 249.705Z" fill="#FF4D00"/>
<path d="M120.4 167.802L101.361 156.906L108.252 155.634C109.34 155.453 110.79 155.634 111.516 155.998L130.555 167.076L123.664 168.347C122.395 168.347 121.126 168.347 120.4 167.802Z" fill="#FF4D00"/>
<path d="M158.661 157.087L141.253 147.099L134.182 148.915L151.589 159.085L158.661 157.087Z" fill="#FF4D00"/>
<path d="M161.018 168.528L129.286 150.005L122.939 151.821L154.853 170.163L161.018 168.528Z" fill="#54565A"/>
<path d="M164.281 178.698L119.493 152.91L113.328 154.545L158.115 180.514L164.281 178.698Z" fill="#54565A"/>
<path d="M156.847 249.705L28.4688 175.429V209.934L156.847 288.568V249.705Z" fill="white"/>
<path d="M87.581 223.917L60.0195 208.118V217.38L87.581 233.179V223.917Z" fill="#FF4D00"/>
<path d="M126.566 249.705L89.2129 228.094V234.451L126.566 256.061V249.705Z" fill="#54565A"/>
<path d="M268.907 219.559V254.064L156.848 288.569V249.705L268.907 219.559Z" fill="#939598"/>
<defs>
<radialGradient id="paint0_radial_493_11687" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(154.664 260.216) rotate(90) scale(39.0156 143.925)">
<stop stop-color="#8F9296"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

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

View File

@ -0,0 +1,81 @@
import styled from 'styled-components'
import { Paragraph } from 'components/Typography'
import { Steps } from 'components/Steps'
import { Form } from 'antd'
import { Button } from 'components/Button'
export const StyledContainer = styled.div`
display: flex;
flex-direction: column;
padding: 25px 40px 58px 40px;
align-items: center;
${props => props.theme.breakpoints.down('s')} {
padding: 15px 0 58px 0;
}
`
export const StyledMain = styled.div`
width: 100%;
max-width: 512px;
${props => props.theme.breakpoints.down('s')} {
max-width: 100%;
}
`
export const StyledStepItemContainer = styled.div`
${props => props.theme.breakpoints.down('s')} {
padding: 0 16px 16px;
}
`
export const StyledSteps = styled(Steps)`
margin-bottom: 13px;
${props => props.theme.breakpoints.up('s')} {
margin-left: -37px;
width: calc(100% + 37px);
margin-bottom: 75px;
}
`
export const StyledParagraph = styled(Paragraph)`
margin-bottom: 40px !important;
${props => props.theme.breakpoints.down('s')} {
margin-bottom: 6px !important;
}
`
export const StyledAccountWrap = styled.div`
padding-top: 10px
`
export const StyledAccountForm = styled(Form)`
margin-top: 37px;
${props => props.theme.breakpoints.down('s')} {
margin-top: 32px;
}
.ant-form-item-required {
width: 100%;
}
`
export const StyledHelp = styled.div`
padding: 0;
color: #1E1D1F;
font-size: 10px;
line-height: 14px;
`
export const StyledSubmitButton = styled(Button)`
width: 358px;
${props => props.theme.breakpoints.down('s')} {
width: 100%;
}
`

View File

@ -0,0 +1,21 @@
import React, { FC } from 'react'
import { StyledContainer, StyledImageWrapper, StyledMainContent, StyledTitle, StyledContent } from './styled'
import { Divider } from 'antd'
import { Button } from 'components/Button'
import { Paths } from 'routes/Paths'
export const Home: FC = () => {
return (
<StyledContainer>
<StyledImageWrapper />
<StyledMainContent>
<StyledTitle level={3}>Welcome to iHealth CheckMeSafe!</StyledTitle>
<StyledContent>
<Button block href={Paths.Activate}>Activate Kit</Button>
<Divider plain>Or</Divider>
<Button block type="default" href={Paths.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,56 @@
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,213 @@
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, Captcha } from 'components/FormControl'
import Image from './images/password@2x.png'
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'
]
// TODO: replace email
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" />
}, {
len: 6,
validateTrigger: 'onSubmit',
message: <ErrorMessage message="The verification code you entered is invalid." />
},
]}
>
<Captcha />
</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' }} alt="" />
<Button block style={{ marginTop: 20 }} onClick={handleLogin}>Log in</Button>
</>
) : null}
</StyledMain>
</StyledWrapper>
</Modal>
)
}

View File

@ -0,0 +1,76 @@
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'
import { Paths } from 'routes/Paths'
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={0}>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={Paths.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,78 @@
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;
${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

@ -0,0 +1,15 @@
import React, { FC, PropsWithChildren } from 'react'
import styled from 'styled-components'
const StyledDiv = styled.div`
padding: 56px 77px;
${props => props.theme.breakpoints.down('s')} {
padding: 43px 20px;
}
`
export const Container: FC<PropsWithChildren> = ({children}) => {
return (
<StyledDiv>{children}</StyledDiv>
)
}

View File

@ -0,0 +1,91 @@
import React, { FC } from 'react'
import styled from 'styled-components'
import { ExternalPaths, Paths } from 'routes/Paths'
import { Button } from 'components/Button'
import { Paragraph } from 'components/Typography'
import { ReactComponent as EmptySvg } from '../images/emptyBox.svg'
const StyledCard = styled.div`
width: 100%;
background: #FFFFFF;
border-radius: 7px;
box-shadow: 2px 1px 4px 1px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row;
padding: 28px 32px;
${props => props.theme.breakpoints.down('s')} {
padding: 0 16px 30px 16px;
flex-direction: column;
}
`
const StyledImageWrapper = styled.div`
> svg {
width: 108px;
height: 89px;
}
${props => props.theme.breakpoints.down('s')} {
text-align: center;
> svg {
width: 113px;
height: 145px;
}
}
`
const StyledInfo = styled.div`
display: flex;
flex-direction: column;
box-sizing: border-box;
justify-content: center;
${props => props.theme.breakpoints.down('s')} {
margin-top: -20px;
}
${props => props.theme.breakpoints.up('s')} {
width: 56%;
max-width: 566px;
padding: 0 20px;
}
`
const StyledParagraph = styled(Paragraph)`
${props => props.theme.breakpoints.up('s')} {
margin-bottom: 10px !important;
}
`
const StyledButtonGroup = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
gap: 15px;
${props => props.theme.breakpoints.down('s')} {
align-items: center;
margin-top: 20px;
}
`
const StyledButton = styled(Button)`
width: 152px;
height: 28px;
font-size: 12px;
line-height: 28px;
`
export const EmptyBlock: FC<{ className?: string; style?: React.CSSProperties }> = (props) => {
return (
<StyledCard {...props}>
<StyledImageWrapper><EmptySvg /></StyledImageWrapper>
<StyledInfo>
<StyledParagraph>Take the next step on your health and wellness journey!</StyledParagraph>
<Paragraph level={2}>Activate your iHealth CheckMeSafe kit, or shop our kits to get started.</Paragraph>
</StyledInfo>
<StyledButtonGroup>
<StyledButton href={Paths.Activate}>Activate Kit</StyledButton>
<StyledButton type="default" href={ExternalPaths.Shop} target="_blank" rel="noopener noreferrer">Shop
Products</StyledButton>
</StyledButtonGroup>
</StyledCard>
)
}

View File

@ -0,0 +1,123 @@
import React, { FC } from 'react'
import styled from 'styled-components'
import { Paragraph } from 'components/Typography'
import { Steps } from 'components/Steps'
import { ExternalPaths } from 'routes/Paths'
import { ReactComponent as BoxSvg } from '../images/box.svg'
import { useBreakpoint } from 'components/Breakpoint'
const StyledCard = styled.div`
background: #FFFFFF;
box-shadow: 2px 1px 4px 1px rgba(0, 0, 0, 0.1);
border-radius: 7px;
padding: 18px 13px;
margin-bottom: 31px;
${props => props.theme.breakpoints.down('s')} {
padding: 17px 0;
}
`
const StyledTitle = styled(Paragraph)`
padding: 0 15px 12px;
`
const StyledVideo = styled.video`
width: 100%;
height: 163px;
margin-top: 12px;
margin-bottom: 8px;
background: #000000;
${props => props.theme.breakpoints.up('s')} {
margin-bottom: 15px;
}
`
const StyledVideoInfo = styled(Paragraph)`
font-size: 14px;
text-align: center;
`
const StyledStep2Content = styled.div`
padding: 13px 28px 24px 18px;
${props => props.theme.breakpoints.down('s')} {
height: 153px;
padding: 0 28px 0 18px;
display: flex;
flex-direction: column;
}
`
const StyledStep2SvgWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
${props => props.theme.breakpoints.down('s')} {
flex: auto;
}
${props => props.theme.breakpoints.up('s')} {
height: 52px;
flex: 0 0 52px;
padding-bottom: 16px;
}
`
const StyledStep2Info = styled.div`
flex: 0 0 auto;
`
export interface InProgressResultItemProps {
step: number
error?: boolean
}
export const InProgressItem: FC<InProgressResultItemProps> = ({ step, error }) => {
const isSmallScreen = useBreakpoint({ s: true, down: true })
return (
<StyledCard>
<StyledTitle>Critical 2 (Kit ID: xxxxxx)</StyledTitle>
<Steps
current={step}
showBar
barStyle={{ marginTop: 12 }}
items={[{
title: 'Activated'
}, {
title: 'Sample Received'
}, {
title: 'Results'
}]}
/>
{step === 0 ? (
<>
<StyledVideo controls src={ExternalPaths.GuideVideo} />
<StyledVideoInfo>Watch video on how to collect your sample</StyledVideoInfo>
</>
) : null}
{step === 1 ? (
<StyledStep2Content>
<StyledStep2SvgWrapper>
<BoxSvg />
</StyledStep2SvgWrapper>
{error ? (
<StyledStep2Info>
<Paragraph primary strong level={2}>ACTION REQUIRED</Paragraph>
<Paragraph level={isSmallScreen ? 2 : 1}>
There was a problem processing your sample. Please check your email for the details and instructions on
how to proceed with the next step.</Paragraph>
</StyledStep2Info>) : (
<StyledStep2Info>
<Paragraph>We are analyzing your sample, the result will be ready soon.</Paragraph>
<Paragraph italic>We will contact you if there are problems processing
your sample.</Paragraph>
</StyledStep2Info>
)}
</StyledStep2Content>
) : null}
</StyledCard>
)
}

View File

@ -0,0 +1,127 @@
import React, { FC } from 'react'
import styled from 'styled-components'
import Icon from '@ant-design/icons'
import { Tag } from 'components/Tag'
import { Paragraph } from 'components/Typography'
import { Button } from 'components/Button'
import { ReactComponent as HeartSvg } from 'icons/heart.svg'
import { ReactComponent as DownloadSvg } from 'icons/download.svg'
const StyledContent = styled.div`
padding: 4px 14px 9px;
background: #FFFFFF;
box-shadow: 2px 1px 4px 1px rgba(0, 0, 0, 0.1);
border-radius: 7px;
margin-bottom: 22px;
${props => props.theme.breakpoints.down('s')} {
padding-top: 16px;
margin-bottom: 25px;
padding-bottom: 10px;
}
`
const StyledItem = styled.div`
margin-bottom: 10px;
margin-left: 1px;
margin-right: 1px;
background: #F5F5F5;
border: 0.5px solid #FF5200;
border-radius: 7px;
padding: 16px;
display: flex;
flex-direction: row;
${props => props.theme.breakpoints.down('s')} {
padding-left: 14px;
padding-right: 7px;
}
`
const StyledName = styled.div`
flex: auto;
`
const StyledTagContent = styled.div`
padding-left: 16px;
flex: 0 0 auto;
${props => props.theme.breakpoints.down('s')} {
padding-left: 8px;
}
`
const StyledTitleContent = styled.div`
display: flex;
flex-direction: row;
padding-bottom: 8px;
${props => props.theme.breakpoints.down('s')} {
padding-bottom: 13px;
}
`
const StyledTitleMain = styled.div`
flex: auto;
display: flex;
flex-direction: row;
`
const StyledIcon = styled(Icon)`
width: 16px;
height: 14px;
padding-top: 5px;
display: inline-block;
${props => props.theme.breakpoints.down('s')} {
padding-top: 6px;
}
`
const StyledTitleInfo = styled.div`
padding-left: 5px;
flex: 1;
`
const StyledParagraph = styled(Paragraph)``
const StyledDownloadButton = styled(Button)`
height: 25px;
line-height: 25px;
font-weight: 400;
font-size: 12px;
width: 108px;
border-radius: 8px;
`
const StyledDownloadContent = styled.div`
padding-left: 18px;
padding-top: 11px;
flex: 0 0 auto;
${props => props.theme.breakpoints.down('s')} {
padding-top: 15px;
}
`
export interface ResultItem {
}
export const ResultItem: FC<ResultItem> = () => {
return (
<StyledContent>
<StyledTitleContent>
<StyledTitleMain>
<StyledIcon component={HeartSvg} />
<StyledTitleInfo>
<StyledParagraph>Critical 2 (Kit ID: xxxxxx)</StyledParagraph>
<StyledParagraph level={2}>Report Date: 05/13/2023</StyledParagraph>
</StyledTitleInfo>
</StyledTitleMain>
<StyledDownloadContent>
<StyledDownloadButton>
Download
<Icon component={DownloadSvg} />
</StyledDownloadButton>
</StyledDownloadContent>
</StyledTitleContent>
<StyledItem>
<StyledName>Infection Name</StyledName>
<StyledTagContent><Tag>Not Detected</Tag></StyledTagContent>
</StyledItem>
</StyledContent>
)
}

View File

@ -0,0 +1,82 @@
import React, { useState } from 'react'
import { EmptyBlock } from '../components/EmptyBlock'
import { Container } from '../components/Container'
import { InProgressItem } from '../components/InProgressItem'
import styled from 'styled-components'
import { Breakpoint } from '../../../components/Breakpoint'
import { Title } from '../../../components/Typography'
import { observer } from 'mobx-react-lite'
import { AuthStore } from '../../../stores/AuthStore'
import { ResultItem } from '../components/ResultItem'
const StyledMain = styled.div`
padding-top: 50px;
${props => props.theme.breakpoints.down('s')} {
padding-top: 32px;
}
`
const StyledItemWrap = styled.div`
display: flex;
flex-direction: row;
${props => props.theme.breakpoints.down('s')} {
flex-direction: column;
}
`
const StyledProgress = styled.div`
${props => props.theme.breakpoints.up('s')} {
width: 351px;
padding-right: 51px;
}
`
const StyledRecent = styled.div`
flex: auto;
`
const StyledSubTitle = styled(Title)`
margin-bottom: 14px;
${props => props.theme.breakpoints.down('s')} {
padding-left: 15px;
}
`
const StyledEmptyBlock = styled(EmptyBlock)`
margin-bottom: 32px`
export const Dashboard = observer(() => {
const [{ userInfo }] = useState(AuthStore.instance())
const [inProgress, setInProgress] = useState([])
const [history, setHistory] = useState(['1'])
return (
<Container>
<Breakpoint s up>
<Title level={0}>Hello, {userInfo?.firstName}!</Title>
</Breakpoint>
<Breakpoint s down>
<Title level={2} style={{ marginBottom: 10 }}>Hello, {userInfo?.firstName}!</Title>
</Breakpoint>
<StyledMain>
{!inProgress.length && <StyledEmptyBlock />}
<StyledItemWrap>
{!!inProgress.length &&
(
<StyledProgress>
<StyledSubTitle level={3}>Tests in Progress</StyledSubTitle>
<InProgressItem step={0} />
<InProgressItem step={1} />
<InProgressItem step={1} error />
</StyledProgress>
)}
{!!history.length && (
<StyledRecent>
<StyledSubTitle level={3}>Recent Test Results</StyledSubTitle>
<ResultItem />
<ResultItem />
<ResultItem />
</StyledRecent>
)}
</StyledItemWrap>
</StyledMain>
</Container>
)
})

View File

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

View File

@ -0,0 +1,38 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { Breakpoint } from 'components/Breakpoint'
import { Paragraph, Title } from 'components/Typography'
import { EmptyBlock } from '../components/EmptyBlock'
import { Container } from '../components/Container'
import { ResultItem } from '../components/ResultItem'
const StyledMain = styled.div`
padding-top: 43px;
${props => props.theme.breakpoints.down('s')} {
padding-top: 23px;
}
`
export const History = () => {
const [testLength] = useState(2)
const [infectionNum] = useState(3)
return (
<Container>
<Breakpoint s up>
<Title level={0}>Test History</Title>
</Breakpoint>
<Breakpoint s down>
<Title level={3} style={{ marginBottom: 10 }}>Test History</Title>
</Breakpoint>
<Paragraph>{testLength === 0 ? 'You do not have any past tests.' : `You have completed ${testLength} health tests, covering ${infectionNum} different infections.`} </Paragraph>
<StyledMain>
{testLength === 0 ? <EmptyBlock /> : (
<>
<ResultItem />
<ResultItem />
</>
)}
</StyledMain>
</Container>
)
}

View File

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

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.425 15.504L31.8 4.21885C31.2489 3.91436 30.6296 3.75464 30 3.75464C29.3704 3.75464 28.7511 3.91436 28.2 4.21885L7.575 15.5087C6.98599 15.831 6.4943 16.3055 6.15129 16.8827C5.80828 17.4598 5.62652 18.1185 5.625 18.7899V41.2056C5.62652 41.877 5.80828 42.5357 6.15129 43.1129C6.4943 43.69 6.98599 44.1645 7.575 44.4868L28.2 55.7767C28.7511 56.0812 29.3704 56.2409 30 56.2409C30.6296 56.2409 31.2489 56.0812 31.8 55.7767L52.425 44.4868C53.014 44.1645 53.5057 43.69 53.8487 43.1129C54.1917 42.5357 54.3735 41.877 54.375 41.2056V18.7923C54.3747 18.1197 54.1936 17.4595 53.8505 16.881C53.5074 16.3025 53.0151 15.8269 52.425 15.504ZM30 7.5001L48.8297 17.8126L41.8523 21.6329L23.0203 11.3204L30 7.5001ZM30 28.1251L11.1703 17.8126L19.1156 13.4626L37.9453 23.7751L30 28.1251ZM9.375 21.0939L28.125 31.3548V51.4618L9.375 41.2079V21.0939ZM50.625 41.1985L31.875 51.4618V31.3642L39.375 27.2603V35.6251C39.375 36.1224 39.5725 36.5993 39.9242 36.9509C40.2758 37.3026 40.7527 37.5001 41.25 37.5001C41.7473 37.5001 42.2242 37.3026 42.5758 36.9509C42.9275 36.5993 43.125 36.1224 43.125 35.6251V25.2071L50.625 21.0939V41.1962V41.1985Z" fill="#FF5200"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,25 @@
<svg width="113" height="145" viewBox="0 0 113 145" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M58.0656 112.717C87.9163 112.717 112.115 106.133 112.115 98.0118C112.115 89.8904 87.9163 83.3066 58.0656 83.3066C28.215 83.3066 4.01624 89.8904 4.01624 98.0118C4.01624 106.133 28.215 112.717 58.0656 112.717Z" fill="url(#paint0_radial_516_9305)"/>
<path d="M18.788 51.4341C18.9241 50.8869 18.788 50.3397 18.4476 49.9293L10.6874 40.7643C9.80243 39.6699 8.03255 40.1487 7.69219 41.5166C7.55604 42.0638 7.69219 42.611 8.03255 43.0213L15.7928 52.118C16.6777 53.2124 18.4476 52.802 18.788 51.4341Z" fill="#D9D9D9"/>
<path d="M71.3868 44.5183C72.1356 44.7235 72.8844 44.4499 73.3609 43.8344L86.2947 26.9405C87.1115 25.9145 86.5669 24.4098 85.3416 24.0679C84.5928 23.8627 83.844 24.1362 83.3675 24.7518L70.4338 41.6457C69.5489 42.74 70.0934 44.2447 71.3868 44.5183Z" fill="#D9D9D9"/>
<path d="M44.4276 43.5603C44.7679 42.9448 44.7679 42.2608 44.3595 41.7136L32.2426 25.025C31.4939 23.999 29.9282 24.0674 29.2475 25.1618C28.9071 25.7773 28.9071 26.4613 29.3155 27.0084L41.4324 43.6287C42.1812 44.6547 43.7469 44.5863 44.4276 43.5603Z" fill="#D9D9D9"/>
<path d="M92.7825 55.4011C93.3271 55.4695 93.8717 55.2643 94.212 54.7855L101.972 45.21C102.789 44.2525 102.177 42.7478 100.951 42.611C100.407 42.5426 99.862 42.7478 99.5217 43.2265L91.7614 52.802C90.9446 53.7596 91.4892 55.2643 92.7825 55.4011Z" fill="#D9D9D9"/>
<path d="M32.3344 60.2571C42.6814 66.7547 43.4301 74.2099 30.6326 77.6297L10.6874 66.0708L32.3344 60.2571Z" fill="#54565A"/>
<path d="M58.8826 94.0448L100.883 82.7594L97.2073 80.6391C81.5506 71.5425 52.8241 70.6533 26.9567 75.5778L58.8826 94.0448Z" fill="#54565A"/>
<path d="M32.1982 60.3254C40.3669 65.3868 41.1838 72.842 28.6585 76.5353L10.6874 66.0707L32.1982 60.3254Z" fill="#FF4D00"/>
<path d="M58.8825 94.0449L100.951 82.6911L99.3175 81.7336C83.6609 72.6369 52.5518 71.3373 28.6584 76.5354L58.8825 94.0449Z" fill="#FF4D00"/>
<path d="M45.2001 63.198L38.0525 59.0942L40.6392 58.6155C41.0477 58.5471 41.5922 58.6155 41.8645 58.7523L49.0121 62.9244L46.4254 63.4032C45.9489 63.4032 45.4724 63.4032 45.2001 63.198Z" fill="#FF4D00"/>
<path d="M59.5633 59.1627L53.0284 55.4009L50.3735 56.0848L56.9085 59.915L59.5633 59.1627Z" fill="#FF4D00"/>
<path d="M60.4483 63.4715L48.5356 56.4951L46.1531 57.1791L58.1338 64.0871L60.4483 63.4715Z" fill="#54565A"/>
<path d="M61.6735 67.3019L44.8596 57.5896L42.5452 58.2052L59.359 67.9858L61.6735 67.3019Z" fill="#54565A"/>
<path d="M58.8826 94.0449L10.6874 66.0708V79.0661L58.8826 108.682V94.0449Z" fill="white"/>
<path d="M32.879 84.3326L22.532 78.3821V81.8703L32.879 87.8208V84.3326Z" fill="#FF4D00"/>
<path d="M47.5145 94.0449L33.4916 85.9058V88.2996L47.5145 96.4388V94.0449Z" fill="#54565A"/>
<path d="M100.951 82.6909V95.6862L58.8826 108.681V94.0447L100.951 82.6909Z" fill="#939598"/>
<defs>
<radialGradient id="paint0_radial_516_9305" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(58.0634 98.0035) rotate(90) scale(14.6942 54.0316)">
<stop stop-color="#8F9296"/>
<stop offset="1" stop-color="#D9D9D9" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

27
src/routes/AppRoutes.tsx Normal file
View File

@ -0,0 +1,27 @@
import React, { FC } from 'react'
import { Route, Routes, Navigate } from 'react-router-dom'
import { Paths } from './Paths'
import { HomeLayout } from './HomeLayout'
import { Home } from 'pages/activation/home'
import { Login } from 'pages/activation/login'
import { Activate } from 'pages/activation/activate'
import { Dashboard } from 'pages/customerPortal/dashboard'
import { History } from 'pages/customerPortal/history'
import { RequireAuth } from './RequireAuth'
export const AppRoutes: FC = () => {
return (
<Routes>
<Route path="/" element={<HomeLayout />}>
<Route index element={<Home />} />
<Route path={Paths.Login} element={<Login />} />
<Route path={Paths.Activate} element={<Activate />} />
</Route>
<Route path={Paths.Account} element={<RequireAuth><HomeLayout alwaysShowIcon /></RequireAuth>}>
<Route index element={<Navigate replace to={Paths.Dashboard} />} />
<Route path={Paths.Dashboard} element={<Dashboard />} />
<Route path={Paths.History} element={<History />} />
</Route>
</Routes>
)
}

22
src/routes/HomeLayout.tsx Normal file
View File

@ -0,0 +1,22 @@
import React, { FC, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { Layout } from 'components/Layout'
import { ActivationStore } from 'stores/ActivationStore'
import { Outlet, useMatch } from 'react-router-dom'
import { Paths } from './Paths'
import { useBreakpoint } from '../components/Breakpoint'
export const HomeLayout: FC<{ alwaysShowIcon?: boolean }> = observer(({ alwaysShowIcon }) => {
const [store] = useState(ActivationStore.instance())
const isActive = useMatch(Paths.Activate)
const isSmallScreen = useBreakpoint({
s: true,
down: true
})
const showMenu = !!isActive && isSmallScreen && store.step === store.stepItems.length - 1
return (
<Layout showMenu={alwaysShowIcon || showMenu} showActiveKit={alwaysShowIcon} activeKitHref={Paths.Activate}>
<Outlet />
</Layout>
)
})

19
src/routes/Paths.ts Normal file
View File

@ -0,0 +1,19 @@
export const Paths = {
Login: '/login',
Activate: '/activate',
Account: '/account',
Dashboard: '/account/dashboard',
History: '/account/history',
Settings: '/account/settings',
}
export const ExternalPaths = {
CompanyLink: 'https://ihealthlabs.com/',
Faqs: 'https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2-detail',
ContactUs: 'mailto:support@ihealthlabs.com',
TermsOfUse: 'https://www.iHealthCheckMeSafe.com/TermsOfUse',
PrivacyPolicy: 'https://www.iHealthCheckMSafe.com/PrivacyPolicy',
Shop: 'https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2',
GuideVideo: '#',
Disclaimer: '#'
}

View File

@ -0,0 +1,14 @@
import React, { FC, PropsWithChildren, useState } from 'react'
import { observer } from 'mobx-react-lite'
import { AuthStore } from '../stores/AuthStore'
import { Navigate, useLocation } from 'react-router-dom'
import { Paths } from './Paths'
export const RequireAuth: FC<PropsWithChildren> = observer(({ children }) => {
const [store] = useState(AuthStore.instance())
const location = useLocation()
if (!store.logined) {
return <Navigate to={Paths.Login} state={{ from: location }} replace />
}
return <>{children}</>
})

38
src/routes/menuConfig.ts Normal file
View File

@ -0,0 +1,38 @@
import { ExternalPaths, Paths } from './Paths'
import { ComponentType } from 'react'
import { ReactComponent as HistorySvg } from 'icons/history.svg'
import { ReactComponent as QuestionSvg } from 'icons/question.svg'
import { ReactComponent as BellSvg } from 'icons/bell.svg'
import { ReactComponent as GearSvg } from 'icons/gear.svg'
import { ReactComponent as HomeSvg } from 'icons/home.svg'
export interface MenuItem {
title: string
href: string
target?: string
icon: ComponentType<any>
}
export const menuConfig: MenuItem[] = [{
title: 'Dashboard',
href: Paths.Dashboard,
icon: HomeSvg
}, {
title: 'Test History',
href: Paths.History,
icon: HistorySvg
}, {
title: 'FAQs',
href: ExternalPaths.Faqs,
target: '_blank',
icon: QuestionSvg
}, {
title: 'Settings',
href: Paths.Settings,
icon: GearSvg
}, {
title: 'Contact Us',
href: ExternalPaths.ContactUs,
target: '_blank',
icon: BellSvg
}]

View File

@ -0,0 +1,64 @@
import { makeAutoObservable } from 'mobx'
export class ActivationStore {
static singleton: ActivationStore | null = null
static instance() {
if (!ActivationStore.singleton) {
ActivationStore.singleton = new ActivationStore()
}
return ActivationStore.singleton
}
constructor() {
makeAutoObservable(this)
}
shouldCreateAccount = true
setShouldCreateAccount(shouldCreateAccount: boolean) {
this.shouldCreateAccount = shouldCreateAccount
}
code: string = '';
setCode(code: string) {
this.code = code
}
step: number = 0
get stepItems(): Array<{ title: string }> {
return (this.shouldCreateAccount ? [{
title: 'Account',
}] : []).concat([{
title: 'Tester Info',
},
{
title: 'Review'
},
{
title: 'Done'
}])
}
handlePrev() {
if (this.step === 0) return
this.step = this.step - 1
console.log('step', this.step)
}
handleNext() {
if (this.step === this.stepItems.length - 1) return
this.step = this.step + 1
console.log('step', this.step)
}
testerInfo: any = {}
setTesterInfo(testerInfo: any) {
this.testerInfo = testerInfo
}
}

53
src/stores/AuthStore.ts Normal file
View File

@ -0,0 +1,53 @@
import { makeAutoObservable } from 'mobx'
import storage from '../utils/storage'
import { Token, UserInfo } from '../constants/storage'
export interface IUserInfo {
firstName: string
lastName: string
gender: number
email: string
}
export class AuthStore {
static singleton: AuthStore | null = null
static instance() {
if (!AuthStore.singleton) {
AuthStore.singleton = new AuthStore()
}
return AuthStore.singleton
}
constructor() {
makeAutoObservable(this)
}
get token(): string | null {
return storage.get<string>(Token)
}
get userInfo(): IUserInfo | null {
return storage.get(UserInfo)
}
get userName(): string {
if (!this.userInfo) {
return ''
}
return [this.userInfo.firstName, this.userInfo.lastName].join(' ')
}
get logined() {
console.log(this.token)
return !!this.token
}
register() {}
login() {}
getUserInfo() {}
resetPassword() {}
}

View File

@ -7,6 +7,7 @@ html, body {
font-weight: 500 !important;
}
.ant-btn.ant-btn-icon-only {
width: 50px !important;
height: 50px !important;
@ -14,9 +15,11 @@ html, body {
line-height: 50px !important;
}
.ant-btn-link, .ant-btn-text {
font-size: 12px;
line-height: 16px;
height: 16px;
}
.ant-wave {
@ -27,6 +30,10 @@ html, body {
color: #1E1D1F;
}
.ant-typography i {
font-weight: 300;
}
.ant-modal .ant-modal-close {
top: 9px !important;
right: 9px !important;
@ -47,6 +54,7 @@ html, body {
}
.health-tooltip-highlighted.ant-tooltip .ant-tooltip-inner {
display: inline-flex;
position: relative;
box-shadow: 1px 1px 0px 0 #FF5A0C, -1px -1px 0px 0 #FF5A0C, -1px 1px 0px 0 #FF5A0C, 1px -1px 0px 0 #FF5A0C;
}
@ -57,6 +65,7 @@ html, body {
.health-tooltip.ant-tooltip .ant-tooltip-inner {
position: relative;
display: inline-flex;
box-shadow: 0 2px 0px 0 #000022, 0 2px 0px 0px #000022, 1px 1px 0px 2px #000022;
}
@ -68,6 +77,10 @@ html, body {
box-shadow: none !important;
}
.ant-input:read-only {
color: #CACACC;
}
.ant-input-status-error:not(.ant-input-disabled):not(.ant-input-borderless).ant-input,
.ant-input-affix-wrapper-status-error:not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-borderless).ant-input-affix-wrapper {
outline: 1px solid #FF5200;
@ -122,7 +135,7 @@ html, body {
}
.ant-radio-wrapper .ant-radio-checked .ant-radio-inner::after {
background: #FF5200;
background: #000022;
transform: scale(1);
}
@ -169,6 +182,10 @@ html, body {
border-bottom: 2px solid #fff;
}
.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background: #F3F3F3;
}
.ant-form-item .ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
display: none;
}
@ -190,3 +207,38 @@ html, body {
.ant-form-item {
margin-bottom: 15px;
}
@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;
}
h1.ant-typography, div.ant-typography-h1, div.ant-typography-h1 > textarea, .ant-typography h1,
h2.ant-typography, div.ant-typography-h2, div.ant-typography-h2 > textarea, .ant-typography h2,
div.ant-typography, .ant-typography p {
margin-bottom: 0;
}
.ant-typography + h1.ant-typography, .ant-typography + h2.ant-typography, .ant-typography + h3.ant-typography, .ant-typography + h4.ant-typography, .ant-typography + h5.ant-typography {
margin-top: 0;
}
.ant-steps.ant-steps-label-vertical .ant-steps-item-content {
width: auto;
max-width: 112px;
}
.ant-steps .ant-steps-item:last-child {
flex: 1;
}

View File

@ -1,4 +1,5 @@
import { ThemeConfig } from 'antd'
export const breakpointList = {
xs: 0,
s: 500,
@ -40,6 +41,7 @@ export const Theme: ThemeConfig = {
controlOutlineWidth: 0,
controlOutline: 'transparent',
// primary button
colorBorder: brandPrimary,
colorPrimary: '#FF5A0C',
colorTextLightSolid: white,
colorPrimaryHover: brandPrimary,
@ -52,6 +54,11 @@ export const Theme: ThemeConfig = {
colorBgTextHover: 'transparent',
colorBgTextActive: 'transparent',
},
Divider: {
colorSplit: '#000',
colorText: '#000',
fontSize: 20,
},
Modal: {
borderRadiusLG: 10,
boxShadow: '2px 2px 4px 1px rgba(0, 0, 0, 0.2)',

33
src/utils/storage.ts Normal file
View File

@ -0,0 +1,33 @@
const NameSpace = 'ihealth-kit-registration_';
function getData<T>(key: string, defaultValue?: T): T | null {
const rawData = window.localStorage.getItem(NameSpace + key);
try {
if (rawData) {
return JSON.parse(rawData);
} else {
return defaultValue || null
}
} catch (e) {
return defaultValue || null;
}
}
function setData<T>(key: string, data: T): void {
window.localStorage.setItem(NameSpace + key, JSON.stringify(data));
}
function clearData(key?: string): void {
if (key) {
window.localStorage.removeItem(NameSpace + key);
} else {
window.localStorage.clear()
}
}
const storage = {
get: getData,
set: setData,
clear: clearData
}
export default storage

View File

@ -18,7 +18,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"baseUrl": "src"
},
"include": [
"src"

View File

@ -9561,6 +9561,11 @@ typeface-lato@^1.1.13:
resolved "https://registry.npmmirror.com/typeface-lato/-/typeface-lato-1.1.13.tgz#630ec75394d7fd2c63148005e6e23f2d8014e931"
integrity sha512-sTn5k3+fagGOi8FQfN2MPeUiTdvG0Z3LVqCaQdsg2sYa0fzNteFZussizdwiPxF45OoFN3zdU/ci+PtjolNSPQ==
typeface-roboto@^1.1.13:
version "1.1.13"
resolved "https://registry.npmmirror.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5"
integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==
typescript@^4.9.5:
version "4.9.5"
resolved "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"