feat: menu

This commit is contained in:
EleanorMao
2023-05-01 21:06:49 +08:00
parent 1c232948ed
commit 97fdafc657
21 changed files with 378 additions and 94 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

@ -1,21 +1,11 @@
import React from 'react';
import { Routes, Route } from "react-router-dom";
import { AppRoutes } from './routes/AppRoutes'
import { BreakpointProvider } from './components/Breakpoint'
import { Layout } from './components/Layout'
import { Home } from './pages/activation/home'
import { Login } from './pages/activation/login'
import { Activate } from './pages/activation/activate'
function App() {
return (
<BreakpointProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/activate" element={<Activate />} />
</Route>
</Routes>
<AppRoutes />
</BreakpointProvider>
);
}

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`

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;
@ -74,6 +75,7 @@ const StyledButton = styled(Button)`
right: 156px;
top: 45px;
width: 216px;
line-height: 58px;
height: 58px;
${props => props.theme.breakpoints.down(600)} {
@ -117,7 +119,7 @@ export const Header: FC<HeaderProps> = ({
<StyledButton href={activeKitHref}>Activate Kit</StyledButton>
</Breakpoint>}
<StyledCartWrapper target="_blank" rel="noopener noreferrer"
href="https://ihealthlabs.com/products/checkmesafe-home-collection-kit-C2">
href={ExternalPaths.Shop}>
<Icon component={Cart} />
<StyledText>SHOP</StyledText>
</StyledCartWrapper>

View File

@ -1,8 +1,9 @@
import React, { FC, PropsWithChildren } from 'react'
import React, { FC, PropsWithChildren, useState } from 'react'
import styled from 'styled-components'
import { Header, HeaderProps } from './Header'
import { Footer } from './Footer'
import { Outlet } from 'react-router-dom'
import { Menu } from '../Menu'
import { Breakpoint, useBreakpoint } from '../Breakpoint'
const StyledSection = styled.section`
display: flex;
@ -21,16 +22,64 @@ const StyledFooter = styled(Footer)`
const StyledMain = styled.main`
flex: auto;
`
const StyledMainSection = styled.section`
flex: auto;
display: flex;
flex-direction: column;
`
export const Layout: FC<PropsWithChildren<Omit<HeaderProps, 'className'>>> = ({ children, ...headerProps }) => {
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} />
<StyledHeader {...headerProps} onClickMenu={handleClickMenu} />
<Main>
{headerProps.showMenu && (!isSmallScreen || displayMenu) ?
<Menu onClose={handleCloseMenu} onLogout={handleLogout} /> : null}
<StyledMainSection>
<StyledMain>
<Outlet />
{children}
</StyledMain>
<StyledFooter />
</StyledMainSection>
</Main>
</StyledSection>
)
}

View File

@ -0,0 +1,58 @@
import React, { FC, useState } from 'react'
import { Breakpoint } from '../Breakpoint'
import {
StyledActivateButton, StyledBackground, StyledCloseIcon, StyledCopyright, StyledExtraContent,
StyledIcon,
StyledMenuContainer,
StyledMenuItem,
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'
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}>
<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}>
<StyledIcon component={item.icon} />
{item.title}
</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,98 @@
import styled from 'styled-components'
import { Button } from '../Button'
import Icon from '@ant-design/icons'
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.div`
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;
`
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.div`
height: 64px;
line-height: 64px;
padding: 0 20px;
color: #000022;
font-weight: 500;
font-size: 16px;
letter-spacing: 0.02em;
position: relative;
border-bottom: 2px solid #FDFDFD;
`

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="#000022"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -15,6 +15,7 @@ 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 } = AuthStore.instance()
@ -79,7 +80,7 @@ export const ActivateStarter: FC = observer(() => {
{!logined ?
<>
Already have an account?&nbsp;
<Link href="/login">Log in</Link>
<Link href={Paths.Login}>Log in</Link>
<br />
</> : null}
<Link onClick={handleShowModal}>Cant find your activation code?</Link>

View File

@ -1,14 +1,14 @@
import React, { FC, useState } from 'react'
import { Form, Col, Row, Divider } from 'antd'
import { observer } from 'mobx-react-lite'
import { Button } from 'components/Button'
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 { Breakpoint } from 'components/Breakpoint'
import { ExternalPaths, Paths } from 'routes/Paths'
const StyledTitle = styled(Title)`
${props => props.theme.breakpoints.up('s')} {
@ -33,7 +33,7 @@ export const Account: FC = observer(() => {
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="/login">log
<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}>
@ -125,8 +125,8 @@ export const Account: FC = observer(() => {
},
}),
]}
extra={<StyledHelp>By clicking "Next" you acknowledge the <Link inherit>Privacy Policy</Link> and agree to
our <Link inherit>Terms of Use</Link>.</StyledHelp>}
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>
@ -136,9 +136,9 @@ export const Account: FC = observer(() => {
</StyledAccountForm>
<Breakpoint s up>
<div style={{ textAlign: "center", marginTop: 30 }}>
<Link style={{ padding: '0 17px' }}>Disclaimer</Link>
<Link style={{ padding: '0 17px' }} href={ExternalPaths.Disclaimer} target="_blank" rel="noopener noreferrer">Disclaimer</Link>
<Divider type="vertical" />
<Link style={{ padding: '0 17px' }}>Privacy</Link>
<Link style={{ padding: '0 17px' }} href={ExternalPaths.PrivacyPolicy} target="_blank" rel="noopener noreferrer">Privacy</Link>
</div>
</Breakpoint>
<VerifyModal open={showModal} onClose={handleClose} />

View File

@ -4,6 +4,7 @@ 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;
@ -58,13 +59,14 @@ export const Done: FC = observer(() => {
<Title level={2}>Whats Next?</Title>
<div>
<StyledLinkItem>
<Link>Watch a video on how to collect your sample</Link>
<Link href={ExternalPaths.GuideVideo} target="_blank" rel="noopener noreferrer">Watch a video on how to
collect your sample</Link>
</StyledLinkItem>
<StyledLinkItem>
<Link href="/dashboard">Go to my dashboard</Link>
<Link href={Paths.Dashboard}>Go to my dashboard</Link>
</StyledLinkItem>
<StyledLinkItem>
<Link>View FAQs</Link>
<Link href={ExternalPaths.Faqs} target="_blank" rel="noopener noreferrer">View FAQs</Link>
</StyledLinkItem>
</div>
</>

View File

@ -10,6 +10,7 @@ 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
@ -107,7 +108,7 @@ export const VerifyModal: FC<Props> = observer(({ onClose, ...props }) => {
</Breakpoint>
<StyledRegisterTitle level={2}>Email address is already registered</StyledRegisterTitle>
<StyledParagraph>Please login to your existing account.</StyledParagraph>
<Button htmlType="submit" block href="/login">Log in</Button>
<Button htmlType="submit" block href={Paths.Login}>Log in</Button>
</>
) : (
<>

View File

@ -2,6 +2,7 @@ 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 (
@ -10,9 +11,9 @@ export const Home: FC = () => {
<StyledMainContent>
<StyledTitle level={3}>Welcome to iHealth CheckMeSafe!</StyledTitle>
<StyledContent>
<Button block href="/activate">Activate Kit</Button>
<Button block href={Paths.Activate}>Activate Kit</Button>
<Divider plain>Or</Divider>
<Button block type="default" href="/login">Log in</Button>
<Button block type="default" href={Paths.Login}>Log in</Button>
</StyledContent>
</StyledMainContent>
</StyledContainer>

View File

@ -12,6 +12,7 @@ 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)
@ -64,7 +65,7 @@ export const Login: FC = () => {
<StyledHint>
<Paragraph>
Dont have an account?&nbsp;
<Link href="/activate">Activate your kit</Link>
<Link href={Paths.Activate}>Activate your kit</Link>
</Paragraph>
</StyledHint>
</StyledContent>

View File

@ -0,0 +1,9 @@
import React from 'react'
export const Dashboard = () => {
return (
<div>
Hello World
</div>
)
}

View File

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

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

@ -0,0 +1,24 @@
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'
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={<HomeLayout alwaysShowIcon />}>
<Route index element={<Navigate replace to={Paths.Dashboard} />} />
<Route path={Paths.Dashboard} element={<Dashboard />} />
</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: '#'
}

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
}]